Compare commits
24
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9505011d71 | ||
|
|
2579aaf2db | ||
|
|
2b7784a226 | ||
|
|
8555a97b39 | ||
|
|
c2dd685a4b | ||
|
|
66fc819379 | ||
|
|
4f1f7e15c4 | ||
|
|
a826c52e2e | ||
|
|
8b2c22aaf1 | ||
|
|
aecfee4469 | ||
|
|
9c785a9c5b | ||
|
|
2fe10387ee | ||
|
|
8642b9e615 | ||
|
|
2a604c8719 | ||
|
|
9c0a0a1086 | ||
|
|
1a4256f997 | ||
|
|
80ae0380a2 | ||
|
|
fd01282798 | ||
|
|
f7ba2eaa62 | ||
|
|
9485a95c0e | ||
|
|
f1ab605bb9 | ||
|
|
3472ff9ba3 | ||
|
|
577dc06d55 | ||
|
|
f4771d1495 |
+3
-2
@@ -9,7 +9,7 @@ In order to use Umbraco as a CMS and build your website with it, you should not
|
||||
- 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 releases page](https://releases.umbraco.com) and start using Umbraco CMS as intended.
|
||||
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.
|
||||
|
||||
## Table of contents
|
||||
|
||||
@@ -79,12 +79,13 @@ Conversely, if you are working on front-end only, you want to build the back-end
|
||||
"AuthorizeCallbackLogoutPathName": "/logout",
|
||||
"AuthorizeCallbackErrorPathName": "/error",
|
||||
"BackOfficeTokenCookie": {
|
||||
"Enabled": true,
|
||||
"SameSite": "None"
|
||||
}
|
||||
```
|
||||
|
||||
> [!NOTE]
|
||||
> If you get stuck in a login loop, try clearing your browser cookies for localhost, and make sure that the `Umbraco:Cms:Security:BackOfficeTokenCookie:SameSite` setting is set to `None`.
|
||||
> If you get stuck in a login loop, try clearing your browser cookies for localhost, and make sure that the `BackOfficeTokenCookie` settings are correct. Namely, that `SameSite` should be set to `None` when running the front-end server separately.
|
||||
|
||||
Then run Umbraco from the command line.
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ Great question! The short version goes like this:
|
||||
|
||||
1. **Switch to the correct branch**
|
||||
|
||||
Switch to the `main` branch
|
||||
Switch to the `contrib` branch
|
||||
|
||||
1. **Build**
|
||||
|
||||
@@ -32,7 +32,7 @@ Great question! The short version goes like this:
|
||||
|
||||
1. **Branch**
|
||||
|
||||
Create a new branch now and name it after the issue you're fixing, we usually follow the format: `temp-12345`. This means it's a temporary branch for the particular issue you're working on, in this case issue number `12345`. Don't commit to `main`, create a new branch first.
|
||||
Create a new branch now and name it after the issue you're fixing, we usually follow the format: `temp-12345`. This means it's a temporary branch for the particular issue you're working on, in this case issue number `12345`. Don't commit to `contrib`, create a new branch first.
|
||||
|
||||
1. **Change**
|
||||
|
||||
@@ -42,7 +42,7 @@ Great question! The short version goes like this:
|
||||
|
||||
Done? Yay! 🎉
|
||||
|
||||
Remember to commit to your new `temp` branch, and don't commit to `main`. Then you can push the changes up to your fork on GitHub.
|
||||
Remember to commit to your new `temp` branch, and don't commit to `contrib`. Then you can push the changes up to your fork on GitHub.
|
||||
|
||||
#### Keeping your Umbraco fork in sync with the main repository
|
||||
[sync fork]: #keeping-your-umbraco-fork-in-sync-with-the-main-repository
|
||||
@@ -59,10 +59,10 @@ Then when you want to get the changes from the main repository:
|
||||
|
||||
```
|
||||
git fetch upstream
|
||||
git rebase upstream/main
|
||||
git rebase upstream/contrib
|
||||
```
|
||||
|
||||
In this command we're syncing with the `main` branch, but you can of course choose another one if needed.
|
||||
In this command we're syncing with the `contrib` branch, but you can of course choose another one if needed.
|
||||
|
||||
[More information on how this works can be found on the thoughtbot blog.][sync fork ext]
|
||||
|
||||
@@ -79,7 +79,7 @@ You can get in touch with [the core contributors team][core collabs] in multiple
|
||||
|
||||
- If there's an existing issue on the issue tracker then that's a good place to leave questions and discuss how to start or move forward.
|
||||
- If you want to ask questions on some code you've already written you can create a draft pull request, [detailed in a GitHub blog post][draft prs].
|
||||
- Unsure where to start? Did something not work as expected? Try leaving a note in the [forum][forum]. The team monitors that one closely, so one of us will be on hand and ready to point you in the right direction.
|
||||
- Unsure where to start? Did something not work as expected? Try leaving a note in the ["Contributing to Umbraco"][contrib forum] forum. The team monitors that one closely, so one of us will be on hand and ready to point you in the right direction.
|
||||
|
||||
|
||||
<!-- Local -->
|
||||
@@ -90,7 +90,7 @@ You can get in touch with [the core contributors team][core collabs] in multiple
|
||||
|
||||
[sync fork ext]: http://robots.thoughtbot.com/post/5133345960/keeping-a-git-fork-updated "Details on keeping a git fork updated"
|
||||
[draft prs]: https://github.blog/2019-02-14-introducing-draft-pull-requests/ "Github's blog post providing details on draft pull requests"
|
||||
[forum]: https://forum.umbraco.com/
|
||||
[contrib forum]: https://our.umbraco.com/forum/contributing-to-umbraco-cms/
|
||||
[Umbraco CMS repo]: https://github.com/umbraco/Umbraco-CMS
|
||||
[up for grabs issues]: https://github.com/umbraco/Umbraco-CMS/issues?q=is%3Aissue+is%3Aopen+label%3Acommunity%2Fup-for-grabs
|
||||
[issue tracker]: https://github.com/umbraco/Umbraco-CMS/issues
|
||||
@@ -115,7 +115,7 @@ Save the changes and return to the Backoffice to see the update.
|
||||
|
||||
|
||||
|
||||
1. Commit your changes to a new temporary branch (avoid committing directly to `main`).
|
||||
1. Commit your changes to a new temporary branch (avoid committing directly to `contrib`).
|
||||
|
||||
2. Push the changes to your fork on GitHub.
|
||||
|
||||
|
||||
+98
-118
@@ -6,101 +6,91 @@ Always reference these instructions first and fallback to search or bash command
|
||||
|
||||
Bootstrap, build, and test the repository:
|
||||
|
||||
- Install .NET SDK (version specified in global.json):
|
||||
- `curl -sSL https://dot.net/v1/dotnet-install.sh | bash /dev/stdin --version $(jq -r '.sdk.version' global.json)`
|
||||
- `export PATH="/home/runner/.dotnet:$PATH"`
|
||||
- Install Node.js (version specified in src/Umbraco.Web.UI.Client/.nvmrc):
|
||||
- `curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.0/install.sh | bash`
|
||||
- `export NVM_DIR="$HOME/.nvm" && [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"`
|
||||
- `nvm install $(cat src/Umbraco.Web.UI.Client/.nvmrc) && nvm use $(cat src/Umbraco.Web.UI.Client/.nvmrc)`
|
||||
- Fix shallow clone issue (required for GitVersioning):
|
||||
- `git fetch --unshallow`
|
||||
- Restore packages:
|
||||
- `dotnet restore` -- takes 50 seconds. NEVER CANCEL. Set timeout to 90+ seconds.
|
||||
- Build the solution:
|
||||
- `dotnet build` -- takes 4.5 minutes. NEVER CANCEL. Set timeout to 10+ minutes.
|
||||
- Install and build frontend:
|
||||
- `cd src/Umbraco.Web.UI.Client`
|
||||
- `npm ci --no-fund --no-audit --prefer-offline` -- takes 11 seconds.
|
||||
- `npm run build:for:cms` -- takes 1.25 minutes. NEVER CANCEL. Set timeout to 5+ minutes.
|
||||
- Install and build Login
|
||||
- `cd src/Umbraco.Web.UI.Login`
|
||||
- `npm ci --no-fund --no-audit --prefer-offline`
|
||||
- `npm run build`
|
||||
- Run the application:
|
||||
- `cd src/Umbraco.Web.UI`
|
||||
- `dotnet run --no-build` -- Application runs on https://localhost:44339 and http://localhost:11000
|
||||
|
||||
Check out [BUILD.md](./BUILD.md) for more detailed instructions.
|
||||
- Install .NET SDK (version specified in global.json):
|
||||
- `curl -sSL https://dot.net/v1/dotnet-install.sh | bash /dev/stdin --version $(jq -r '.sdk.version' global.json)`
|
||||
- `export PATH="/home/runner/.dotnet:$PATH"`
|
||||
- Install Node.js (version specified in src/Umbraco.Web.UI.Client/.nvmrc):
|
||||
- `curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.0/install.sh | bash`
|
||||
- `export NVM_DIR="$HOME/.nvm" && [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"`
|
||||
- `nvm install $(cat src/Umbraco.Web.UI.Client/.nvmrc) && nvm use $(cat src/Umbraco.Web.UI.Client/.nvmrc)`
|
||||
- Fix shallow clone issue (required for GitVersioning):
|
||||
- `git fetch --unshallow`
|
||||
- Restore packages:
|
||||
- `dotnet restore` -- takes 50 seconds. NEVER CANCEL. Set timeout to 90+ seconds.
|
||||
- Build the solution:
|
||||
- `dotnet build` -- takes 4.5 minutes. NEVER CANCEL. Set timeout to 10+ minutes.
|
||||
- Install and build frontend:
|
||||
- `cd src/Umbraco.Web.UI.Client`
|
||||
- `npm ci --no-fund --no-audit --prefer-offline` -- takes 11 seconds.
|
||||
- `npm run build:for:cms` -- takes 1.25 minutes. NEVER CANCEL. Set timeout to 5+ minutes.
|
||||
- Install and build Login
|
||||
- `cd src/Umbraco.Web.UI.Login`
|
||||
- `npm ci --no-fund --no-audit --prefer-offline`
|
||||
- `npm run build`
|
||||
- Run the application:
|
||||
- `cd src/Umbraco.Web.UI`
|
||||
- `dotnet run --no-build` -- Application runs on https://localhost:44339 and http://localhost:11000
|
||||
|
||||
## Validation
|
||||
|
||||
- ALWAYS run through at least one complete end-to-end scenario after making changes.
|
||||
- Build and unit tests must pass before committing changes.
|
||||
- Frontend build produces output in src/Umbraco.Web.UI.Client/dist-cms/ which gets copied to src/Umbraco.Web.UI/wwwroot/umbraco/backoffice/
|
||||
- Always run `dotnet build` and `npm run build:for:cms` before running the application to see your changes.
|
||||
- For login-only changes, you can run `npm run build` from src/Umbraco.Web.UI.Login and then `dotnet run --no-build` from src/Umbraco.Web.UI.
|
||||
- For frontend-only changes, you can run `npm run dev:server` from src/Umbraco.Web.UI.Client for hot reloading.
|
||||
- Frontend changes should be linted using `npm run lint:fix` which uses Eslint.
|
||||
- ALWAYS run through at least one complete end-to-end scenario after making changes.
|
||||
- Build and unit tests must pass before committing changes.
|
||||
- Frontend build produces output in src/Umbraco.Web.UI.Client/dist-cms/ which gets copied to src/Umbraco.Web.UI/wwwroot/umbraco/backoffice/
|
||||
- Always run `dotnet build` and `npm run build:for:cms` before running the application to see your changes.
|
||||
- For login-only changes, you can run `npm run build` from src/Umbraco.Web.UI.Login and then `dotnet run --no-build` from src/Umbraco.Web.UI.
|
||||
- For frontend-only changes, you can run `npm run dev:server` from src/Umbraco.Web.UI.Client for hot reloading.
|
||||
- Frontend changes should be linted using `npm run lint:fix` which uses Eslint.
|
||||
|
||||
## Testing
|
||||
|
||||
### Unit Tests (.NET)
|
||||
|
||||
- Location: tests/Umbraco.Tests.UnitTests/
|
||||
- Run: `dotnet test tests/Umbraco.Tests.UnitTests/Umbraco.Tests.UnitTests.csproj --configuration Release --verbosity minimal`
|
||||
- Duration: ~1 minute with 3,343 tests
|
||||
- NEVER CANCEL: Set timeout to 5+ minutes
|
||||
- Location: tests/Umbraco.Tests.UnitTests/
|
||||
- Run: `dotnet test tests/Umbraco.Tests.UnitTests/Umbraco.Tests.UnitTests.csproj --configuration Release --verbosity minimal`
|
||||
- Duration: ~1 minute with 3,343 tests
|
||||
- NEVER CANCEL: Set timeout to 5+ minutes
|
||||
|
||||
### Integration Tests (.NET)
|
||||
|
||||
- Location: tests/Umbraco.Tests.Integration/
|
||||
- Run: `dotnet test tests/Umbraco.Tests.Integration/Umbraco.Tests.Integration.csproj --configuration Release --verbosity minimal`
|
||||
- NEVER CANCEL: Set timeout to 10+ minutes
|
||||
- Location: tests/Umbraco.Tests.Integration/
|
||||
- Run: `dotnet test tests/Umbraco.Tests.Integration/Umbraco.Tests.Integration.csproj --configuration Release --verbosity minimal`
|
||||
- NEVER CANCEL: Set timeout to 10+ minutes
|
||||
|
||||
### Frontend Tests
|
||||
|
||||
- Location: src/Umbraco.Web.UI.Client/
|
||||
- Run: `npm test` (requires `npx playwright install` first)
|
||||
- Frontend tests use Web Test Runner with Playwright
|
||||
- Location: src/Umbraco.Web.UI.Client/
|
||||
- Run: `npm test` (requires `npx playwright install` first)
|
||||
- Frontend tests use Web Test Runner with Playwright
|
||||
|
||||
### Acceptance Tests (E2E)
|
||||
|
||||
- Location: tests/Umbraco.Tests.AcceptanceTest/
|
||||
- Requires running Umbraco application and configuration
|
||||
- See tests/Umbraco.Tests.AcceptanceTest/README.md for detailed setup (requires `npx playwright install` first)
|
||||
- Location: tests/Umbraco.Tests.AcceptanceTest/
|
||||
- Requires running Umbraco application and configuration
|
||||
- See tests/Umbraco.Tests.AcceptanceTest/README.md for detailed setup (requires `npx playwright install` first)
|
||||
|
||||
## Project Structure
|
||||
|
||||
The solution contains 30 C# projects organized as follows:
|
||||
|
||||
### Main Application Projects
|
||||
|
||||
- **Umbraco.Web.UI**: Main web application project (startup project)
|
||||
- **Umbraco.Web.UI.Client**: TypeScript frontend (backoffice)
|
||||
- **Umbraco.Web.UI.Login**: Separate login screen frontend
|
||||
- **Umbraco.Core**: Core domain models and interfaces
|
||||
- **Umbraco.Infrastructure**: Data access and infrastructure
|
||||
- **Umbraco.Cms**: Main CMS package
|
||||
- **Umbraco.Web.UI**: Main web application project (startup project)
|
||||
- **Umbraco.Web.UI.Client**: TypeScript frontend (backoffice)
|
||||
- **Umbraco.Web.UI.Login**: Separate login screen frontend
|
||||
- **Umbraco.Core**: Core domain models and interfaces
|
||||
- **Umbraco.Infrastructure**: Data access and infrastructure
|
||||
- **Umbraco.Cms**: Main CMS package
|
||||
|
||||
### API Projects
|
||||
|
||||
- **Umbraco.Cms.Api.Management**: Management API
|
||||
- **Umbraco.Cms.Api.Delivery**: Content Delivery API
|
||||
- **Umbraco.Cms.Api.Common**: Shared API components
|
||||
- **Umbraco.Cms.Api.Management**: Management API
|
||||
- **Umbraco.Cms.Api.Delivery**: Content Delivery API
|
||||
- **Umbraco.Cms.Api.Common**: Shared API components
|
||||
|
||||
### Persistence Projects
|
||||
|
||||
- **Umbraco.Cms.Persistence.SqlServer**: SQL Server support
|
||||
- **Umbraco.Cms.Persistence.Sqlite**: SQLite support
|
||||
- **Umbraco.Cms.Persistence.EFCore**: Entity Framework Core abstractions
|
||||
- **Umbraco.Cms.Persistence.SqlServer**: SQL Server support
|
||||
- **Umbraco.Cms.Persistence.Sqlite**: SQLite support
|
||||
- **Umbraco.Cms.Persistence.EFCore**: Entity Framework Core abstractions
|
||||
|
||||
### Test Projects
|
||||
|
||||
- **Umbraco.Tests.UnitTests**: Unit tests
|
||||
- **Umbraco.Tests.Integration**: Integration tests
|
||||
- **Umbraco.Tests.AcceptanceTest**: End-to-end tests with Playwright
|
||||
- **Umbraco.Tests.Common**: Shared test utilities
|
||||
- **Umbraco.Tests.UnitTests**: Unit tests
|
||||
- **Umbraco.Tests.Integration**: Integration tests
|
||||
- **Umbraco.Tests.AcceptanceTest**: End-to-end tests with Playwright
|
||||
- **Umbraco.Tests.Common**: Shared test utilities
|
||||
|
||||
## Common Tasks
|
||||
|
||||
@@ -108,7 +98,6 @@ The solution contains 30 C# projects organized as follows:
|
||||
|
||||
**Production Mode (Standard Development)**
|
||||
Use this for backend development, testing full builds, or when you don't need hot reloading:
|
||||
|
||||
1. Build frontend assets: `cd src/Umbraco.Web.UI.Client && npm run build:for:cms`
|
||||
2. Run backend: `cd src/Umbraco.Web.UI && dotnet run --no-build`
|
||||
3. Access backoffice: `https://localhost:44339/umbraco`
|
||||
@@ -116,17 +105,17 @@ Use this for backend development, testing full builds, or when you don't need ho
|
||||
|
||||
**Vite Dev Server Mode (Frontend Development with Hot Reload)**
|
||||
Use this for frontend-only development with hot module reloading:
|
||||
|
||||
1. Configure backend for frontend development - Add to `src/Umbraco.Web.UI/appsettings.json` under `Umbraco:CMS:Security`:
|
||||
```json
|
||||
"BackOfficeHost": "http://localhost:5173",
|
||||
"AuthorizeCallbackPathName": "/oauth_complete",
|
||||
"AuthorizeCallbackLogoutPathName": "/logout",
|
||||
"AuthorizeCallbackErrorPathName": "/error",
|
||||
"BackOfficeTokenCookie": {
|
||||
"SameSite": "None"
|
||||
}
|
||||
```
|
||||
```json
|
||||
"BackOfficeHost": "http://localhost:5173",
|
||||
"AuthorizeCallbackPathName": "/oauth_complete",
|
||||
"AuthorizeCallbackLogoutPathName": "/logout",
|
||||
"AuthorizeCallbackErrorPathName": "/error",
|
||||
"BackOfficeTokenCookie": {
|
||||
"Enabled": true,
|
||||
"SameSite": "None"
|
||||
}
|
||||
```
|
||||
2. Run backend: `cd src/Umbraco.Web.UI && dotnet run --no-build`
|
||||
3. Run frontend dev server: `cd src/Umbraco.Web.UI.Client && npm run dev:server`
|
||||
4. Access backoffice: `http://localhost:5173/` (no `/umbraco` prefix)
|
||||
@@ -135,48 +124,39 @@ Use this for frontend-only development with hot module reloading:
|
||||
**Important:** Remove the `BackOfficeHost` configuration before committing or switching back to production mode.
|
||||
|
||||
### Backend-Only Development
|
||||
|
||||
For backend-only changes, disable frontend builds:
|
||||
|
||||
- Comment out the target named "BuildStaticAssetsPreconditions" in src/Umbraco.Cms.StaticAssets.csproj:
|
||||
```
|
||||
<!--<Target Name="BuildStaticAssetsPreconditions" BeforeTargets="AssignTargetPaths">
|
||||
[...]
|
||||
</Target>-->
|
||||
```
|
||||
- Remember to uncomment before committing
|
||||
- Comment out the target named "BuildStaticAssetsPreconditions" in src/Umbraco.Cms.StaticAssets.csproj:
|
||||
```
|
||||
<!--<Target Name="BuildStaticAssetsPreconditions" BeforeTargets="AssignTargetPaths">
|
||||
[...]
|
||||
</Target>-->
|
||||
```
|
||||
- Remember to uncomment before committing
|
||||
|
||||
### Building NuGet Packages
|
||||
|
||||
To build custom NuGet packages for testing:
|
||||
|
||||
```bash
|
||||
dotnet pack -c Release -o Build.Out
|
||||
dotnet nuget add source [Path to Build.Out folder] -n MyLocalFeed
|
||||
```
|
||||
|
||||
### Regenerating Frontend API Types
|
||||
|
||||
When changing Management API:
|
||||
|
||||
```bash
|
||||
cd src/Umbraco.Web.UI.Client
|
||||
npm run generate:server-api-dev
|
||||
```
|
||||
|
||||
Also update OpenApi.json from /umbraco/swagger/management/swagger.json
|
||||
|
||||
## Database Setup
|
||||
|
||||
Default configuration supports SQLite for development. For production-like testing:
|
||||
|
||||
- Use SQL Server/LocalDb for better performance
|
||||
- Configure connection string in src/Umbraco.Web.UI/appsettings.json
|
||||
- Use SQL Server/LocalDb for better performance
|
||||
- Configure connection string in src/Umbraco.Web.UI/appsettings.json
|
||||
|
||||
## Clean Up / Reset
|
||||
|
||||
To reset development environment:
|
||||
|
||||
```bash
|
||||
# Remove configuration and database
|
||||
rm src/Umbraco.Web.UI/appsettings.json
|
||||
@@ -188,31 +168,31 @@ git clean -xdf .
|
||||
|
||||
## Version Information
|
||||
|
||||
- Target Framework: .NET (version specified in global.json)
|
||||
- Current Version: (specified in version.json)
|
||||
- Node.js Requirement: (specified in src/Umbraco.Web.UI.Client/.nvmrc)
|
||||
- npm Requirement: Latest compatible version
|
||||
- Target Framework: .NET (version specified in global.json)
|
||||
- Current Version: (specified in version.json)
|
||||
- Node.js Requirement: (specified in src/Umbraco.Web.UI.Client/.nvmrc)
|
||||
- npm Requirement: Latest compatible version
|
||||
|
||||
## Known Issues
|
||||
|
||||
- Build requires full git history (not shallow clone) due to GitVersioning
|
||||
- Some NuGet package security warnings are expected (SixLabors.ImageSharp vulnerabilities)
|
||||
- Frontend tests require Playwright browser installation: `npx playwright install`
|
||||
- Older Node.js versions may show engine compatibility warnings (check .nvmrc for current requirement)
|
||||
- Build requires full git history (not shallow clone) due to GitVersioning
|
||||
- Some NuGet package security warnings are expected (SixLabors.ImageSharp vulnerabilities)
|
||||
- Frontend tests require Playwright browser installation: `npx playwright install`
|
||||
- Older Node.js versions may show engine compatibility warnings (check .nvmrc for current requirement)
|
||||
|
||||
## Timing Expectations
|
||||
|
||||
**NEVER CANCEL** these operations - they are expected to take time:
|
||||
|
||||
| Operation | Expected Time | Timeout Setting |
|
||||
| ----------------------- | ------------- | --------------- |
|
||||
| `dotnet restore` | 50 seconds | 90+ seconds |
|
||||
| `dotnet build` | 4.5 minutes | 10+ minutes |
|
||||
| `npm ci` | 11 seconds | 30+ seconds |
|
||||
| `npm run build:for:cms` | 1.25 minutes | 5+ minutes |
|
||||
| `npm test` | 2 minutes | 5+ minutes |
|
||||
| `npm run lint` | 1 minute | 5+ minutes |
|
||||
| Unit tests | 1 minute | 5+ minutes |
|
||||
| Integration tests | Variable | 10+ minutes |
|
||||
| Operation | Expected Time | Timeout Setting |
|
||||
|-----------|--------------|-----------------|
|
||||
| `dotnet restore` | 50 seconds | 90+ seconds |
|
||||
| `dotnet build` | 4.5 minutes | 10+ minutes |
|
||||
| `npm ci` | 11 seconds | 30+ seconds |
|
||||
| `npm run build:for:cms` | 1.25 minutes | 5+ minutes |
|
||||
| `npm test` | 2 minutes | 5+ minutes |
|
||||
| `npm run lint` | 1 minute | 5+ minutes |
|
||||
| Unit tests | 1 minute | 5+ minutes |
|
||||
| Integration tests | Variable | 10+ minutes |
|
||||
|
||||
Always wait for commands to complete rather than canceling and retrying.
|
||||
Always wait for commands to complete rather than canceling and retrying.
|
||||
@@ -4,7 +4,6 @@ on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- release/*
|
||||
- v*/dev
|
||||
- v*/main
|
||||
paths:
|
||||
@@ -16,7 +15,6 @@ on:
|
||||
types: [opened, synchronize, reopened, closed]
|
||||
branches:
|
||||
- main
|
||||
- release/*
|
||||
- v*/dev
|
||||
- v*/main
|
||||
workflow_dispatch:
|
||||
|
||||
Vendored
+1
@@ -107,6 +107,7 @@
|
||||
"UMBRACO__CMS__SECURITY__AUTHORIZECALLBACKLOGOUTPATHNAME": "/logout",
|
||||
"UMBRACO__CMS__SECURITY__AUTHORIZECALLBACKERRORPATHNAME": "/error",
|
||||
"UMBRACO__CMS__SECURITY__KEEPUSERLOGGEDIN": "true",
|
||||
"UMBRACO__CMS__SECURITY__BACKOFFICETOKENCOOKIE__ENABLED": "true",
|
||||
"UMBRACO__CMS__SECURITY__BACKOFFICETOKENCOOKIE__SAMESITE": "None"
|
||||
},
|
||||
"sourceFileMap": {
|
||||
|
||||
Vendored
-3
@@ -1,8 +1,5 @@
|
||||
{
|
||||
"cSpell.words": [
|
||||
"backoffice",
|
||||
"pickable",
|
||||
"Pickable",
|
||||
"unprovide",
|
||||
"Unproviding"
|
||||
],
|
||||
|
||||
@@ -1,336 +0,0 @@
|
||||
# Umbraco CMS - Multi-Project Repository
|
||||
|
||||
Enterprise-grade CMS built on .NET 10.0. This repository contains 21 production projects organized in a layered architecture with clear separation of concerns.
|
||||
|
||||
**Repository**: https://github.com/umbraco/Umbraco-CMS
|
||||
**License**: MIT
|
||||
**Main Branch**: `main`
|
||||
|
||||
---
|
||||
|
||||
## 1. Overview
|
||||
|
||||
### What This Repository Contains
|
||||
|
||||
**21 Production Projects** organized in 3 main categories:
|
||||
|
||||
1. **Core Architecture** (Domain & Infrastructure)
|
||||
- `Umbraco.Core` - Interface contracts, domain models, notifications
|
||||
- `Umbraco.Infrastructure` - Service implementations, data access, caching
|
||||
|
||||
2. **Web & APIs** (Presentation Layer)
|
||||
- `Umbraco.Web.UI` - Main ASP.NET Core web application
|
||||
- `Umbraco.Web.Common` - Shared web functionality, controllers, middleware
|
||||
- `Umbraco.Cms.Api.Management` - Backoffice Management API (REST)
|
||||
- `Umbraco.Cms.Api.Delivery` - Content Delivery API (headless)
|
||||
- `Umbraco.Cms.Api.Common` - Shared API infrastructure
|
||||
|
||||
3. **Specialized Features** (Pluggable Modules)
|
||||
- Persistence: EF Core (modern), NPoco (legacy) for SQL Server & SQLite
|
||||
- Caching: `PublishedCache.HybridCache` (in-memory + distributed)
|
||||
- Search: `Examine.Lucene` (full-text search)
|
||||
- Imaging: `Imaging.ImageSharp` v1 & v2 (image processing)
|
||||
- Other: Static assets, targets, development tools
|
||||
|
||||
**6 Test Projects**:
|
||||
- `Umbraco.Tests.Common` - Shared test utilities
|
||||
- `Umbraco.Tests.UnitTests` - Unit tests
|
||||
- `Umbraco.Tests.Integration` - Integration tests
|
||||
- `Umbraco.Tests.Benchmarks` - Performance benchmarks
|
||||
- `Umbraco.Tests.AcceptanceTest` - E2E tests
|
||||
- `Umbraco.Tests.AcceptanceTest.UmbracoProject` - Test instance
|
||||
|
||||
### Key Technologies
|
||||
|
||||
- **.NET 10.0** - Target framework for all projects
|
||||
- **ASP.NET Core** - Web framework
|
||||
- **Entity Framework Core** - Modern ORM
|
||||
- **OpenIddict** - OAuth 2.0/OpenID Connect authentication
|
||||
- **Swashbuckle** - OpenAPI/Swagger documentation
|
||||
- **Lucene.NET** - Full-text search via Examine
|
||||
- **ImageSharp** - Image processing
|
||||
|
||||
---
|
||||
|
||||
## 2. Repository Structure
|
||||
|
||||
```
|
||||
Umbraco-CMS/
|
||||
├── src/ # 21 production projects
|
||||
│ ├── Umbraco.Core/ # Domain contracts (interfaces only)
|
||||
│ │ └── CLAUDE.md # ⭐ Core architecture guide
|
||||
│ ├── Umbraco.Infrastructure/ # Service implementations
|
||||
│ ├── Umbraco.Web.Common/ # Web utilities
|
||||
│ ├── Umbraco.Web.UI/ # Main web application
|
||||
│ ├── Umbraco.Cms.Api.Management/ # Management API
|
||||
│ ├── Umbraco.Cms.Api.Delivery/ # Delivery API (headless)
|
||||
│ ├── Umbraco.Cms.Api.Common/ # Shared API infrastructure
|
||||
│ │ └── CLAUDE.md # ⭐ API patterns guide
|
||||
│ ├── Umbraco.PublishedCache.HybridCache/ # Content caching
|
||||
│ ├── Umbraco.Examine.Lucene/ # Search indexing
|
||||
│ ├── Umbraco.Cms.Persistence.EFCore/ # EF Core data access
|
||||
│ ├── Umbraco.Cms.Persistence.EFCore.Sqlite/
|
||||
│ ├── Umbraco.Cms.Persistence.EFCore.SqlServer/
|
||||
│ ├── Umbraco.Cms.Persistence.Sqlite/ # Legacy SQLite
|
||||
│ ├── Umbraco.Cms.Persistence.SqlServer/ # Legacy SQL Server
|
||||
│ ├── Umbraco.Cms.Imaging.ImageSharp/ # Image processing v1
|
||||
│ ├── Umbraco.Cms.Imaging.ImageSharp2/ # Image processing v2
|
||||
│ ├── Umbraco.Cms.StaticAssets/ # Embedded assets
|
||||
│ ├── Umbraco.Cms.DevelopmentMode.Backoffice/
|
||||
│ ├── Umbraco.Cms.Targets/ # NuGet targets
|
||||
│ └── Umbraco.Cms/ # Meta-package
|
||||
│
|
||||
├── tests/ # 6 test projects
|
||||
│ ├── Umbraco.Tests.Common/
|
||||
│ ├── Umbraco.Tests.UnitTests/
|
||||
│ ├── Umbraco.Tests.Integration/
|
||||
│ ├── Umbraco.Tests.Benchmarks/
|
||||
│ ├── Umbraco.Tests.AcceptanceTest/
|
||||
│ └── Umbraco.Tests.AcceptanceTest.UmbracoProject/
|
||||
│
|
||||
├── templates/ # Project templates
|
||||
│ └── Umbraco.Templates/
|
||||
│
|
||||
├── tools/ # Build tools
|
||||
│ └── Umbraco.JsonSchema/
|
||||
│
|
||||
├── umbraco.sln # Main solution file
|
||||
├── Directory.Build.props # Shared build configuration
|
||||
├── Directory.Packages.props # Centralized package versions
|
||||
├── .editorconfig # Code style
|
||||
└── .globalconfig # Roslyn analyzers
|
||||
```
|
||||
|
||||
### Architecture Layers
|
||||
|
||||
**Dependency Flow** (unidirectional, always flows inward):
|
||||
|
||||
```
|
||||
Web.UI → Web.Common → Infrastructure → Core
|
||||
↓
|
||||
Api.Management → Api.Common → Infrastructure → Core
|
||||
↓
|
||||
Api.Delivery → Api.Common → Infrastructure → Core
|
||||
```
|
||||
|
||||
**Key Principle**: Core has NO dependencies (pure contracts). Infrastructure implements Core. Web/APIs depend on Infrastructure.
|
||||
|
||||
### Project Dependencies
|
||||
|
||||
**Core Layer**:
|
||||
- `Umbraco.Core` → No dependencies (only Microsoft.Extensions.*)
|
||||
|
||||
**Infrastructure Layer**:
|
||||
- `Umbraco.Infrastructure` → `Umbraco.Core`
|
||||
- `Umbraco.PublishedCache.*` → `Umbraco.Infrastructure`
|
||||
- `Umbraco.Examine.Lucene` → `Umbraco.Infrastructure`
|
||||
- `Umbraco.Cms.Persistence.*` → `Umbraco.Infrastructure`
|
||||
|
||||
**Web Layer**:
|
||||
- `Umbraco.Web.Common` → `Umbraco.Infrastructure` + caching + search
|
||||
- `Umbraco.Web.UI` → `Umbraco.Web.Common` + all features
|
||||
|
||||
**API Layer**:
|
||||
- `Umbraco.Cms.Api.Common` → `Umbraco.Web.Common`
|
||||
- `Umbraco.Cms.Api.Management` → `Umbraco.Cms.Api.Common`
|
||||
- `Umbraco.Cms.Api.Delivery` → `Umbraco.Cms.Api.Common`
|
||||
|
||||
---
|
||||
|
||||
## 3. Teamwork & Collaboration
|
||||
|
||||
### Branching Strategy
|
||||
|
||||
- **Main branch**: `main` (protected)
|
||||
- **Branch naming**:
|
||||
- See `.github/CONTRIBUTING.md` for full guidelines
|
||||
|
||||
### Pull Request Process
|
||||
|
||||
- **PR Template**: `.github/pull_request_template.md`
|
||||
- **Required CI Checks**:
|
||||
- All tests pass
|
||||
- Code formatting (dotnet format)
|
||||
- No build warnings
|
||||
- **Merge Strategy**: Squash and merge (via GitHub UI)
|
||||
- **Reviews**: Required from code owners
|
||||
|
||||
### Commit Messages
|
||||
|
||||
Follow Conventional Commits format:
|
||||
```
|
||||
<type>(<scope>): <description>
|
||||
|
||||
Types: feat, fix, docs, style, refactor, test, chore
|
||||
Scope: project name (core, web, api, etc.)
|
||||
|
||||
Examples:
|
||||
feat(core): add IContentService.GetByIds method
|
||||
fix(api): resolve null reference in schema handler
|
||||
docs(web): update routing documentation
|
||||
```
|
||||
|
||||
### Code Owners
|
||||
|
||||
Project ownership is distributed across teams. Check individual project directories for ownership.
|
||||
|
||||
---
|
||||
|
||||
## 4. Architecture Patterns
|
||||
|
||||
### Core Architectural Decisions
|
||||
|
||||
1. **Layered Architecture with Dependency Inversion**
|
||||
- Core defines contracts (interfaces)
|
||||
- Infrastructure implements contracts
|
||||
- Web/APIs consume implementations via DI
|
||||
|
||||
2. **Interface-First Design**
|
||||
- All services defined as interfaces in Core
|
||||
- Enables testing, polymorphism, extensibility
|
||||
|
||||
3. **Notification Pattern** (not C# events)
|
||||
- See `/src/Umbraco.Core/CLAUDE.md` → "2. Notification System (Event Handling)"
|
||||
|
||||
4. **Composer Pattern** (DI registration)
|
||||
- See `/src/Umbraco.Core/CLAUDE.md` → "3. Composer Pattern (DI Registration)"
|
||||
|
||||
5. **Scoping Pattern** (Unit of Work)
|
||||
- See `/src/Umbraco.Core/CLAUDE.md` → "5. Scoping Pattern (Unit of Work)"
|
||||
|
||||
6. **Attempt Pattern** (operation results)
|
||||
- `Attempt<TResult, TStatus>` instead of exceptions
|
||||
- Strongly-typed operation status enums
|
||||
|
||||
### Key Design Patterns Used
|
||||
|
||||
- **Repository Pattern** - Data access abstraction
|
||||
- **Unit of Work** - Scoping for transactions
|
||||
- **Builder Pattern** - `ProblemDetailsBuilder` for API errors
|
||||
- **Strategy Pattern** - OpenAPI handlers (schema ID, operation ID)
|
||||
- **Options Pattern** - All configuration via `IOptions<T>`
|
||||
- **Factory Pattern** - Content type factories
|
||||
- **Mediator Pattern** - Notification aggregator
|
||||
|
||||
---
|
||||
|
||||
## 5. Project-Specific Notes
|
||||
|
||||
### Centralized Package Management
|
||||
|
||||
**All NuGet package versions** are centralized in `Directory.Packages.props`. Individual projects do NOT specify versions.
|
||||
|
||||
```xml
|
||||
<!-- Individual projects reference WITHOUT version -->
|
||||
<PackageReference Include="Swashbuckle.AspNetCore" />
|
||||
|
||||
<!-- Versions defined in Directory.Packages.props -->
|
||||
<PackageVersion Include="Swashbuckle.AspNetCore" Version="6.5.0" />
|
||||
```
|
||||
|
||||
### Build Configuration
|
||||
|
||||
- `Directory.Build.props` - Shared properties (target framework, company, copyright)
|
||||
- `.editorconfig` - Code style rules
|
||||
- `.globalconfig` - Roslyn analyzer rules
|
||||
|
||||
### Persistence Layer - NPoco and EF Core
|
||||
|
||||
The repository contains BOTH (actively supported):
|
||||
- **Current**: NPoco-based persistence (`Umbraco.Cms.Persistence.Sqlite`, `Umbraco.Cms.Persistence.SqlServer`) - widely used and fully supported
|
||||
- **Future**: EF Core-based persistence (`Umbraco.Cms.Persistence.EFCore.*`) - migration in progress
|
||||
|
||||
**Note**: The codebase is actively migrating to EF Core, but NPoco remains the primary persistence layer and is not deprecated. Both are fully supported.
|
||||
|
||||
### Authentication: OpenIddict
|
||||
|
||||
All APIs use **OpenIddict** (OAuth 2.0/OpenID Connect):
|
||||
- Reference tokens (not JWT) for better security
|
||||
- **Secure cookie-based token storage** (v17+) - tokens stored in HTTP-only cookies with `__Host-` prefix
|
||||
- Tokens are redacted from client-side responses and passed via secure cookies only
|
||||
- ASP.NET Core Data Protection for token encryption
|
||||
- Configured in `Umbraco.Cms.Api.Common`
|
||||
- API requests must include credentials (`credentials: include` for fetch)
|
||||
|
||||
**Load Balancing Requirement**: All servers must share the same Data Protection key ring.
|
||||
|
||||
### Content Caching Strategy
|
||||
|
||||
**HybridCache** (`Umbraco.PublishedCache.HybridCache`):
|
||||
- In-memory cache + distributed cache support
|
||||
- Published content only (not draft)
|
||||
- Invalidated via notifications and cache refreshers
|
||||
|
||||
### API Versioning
|
||||
|
||||
APIs use `Asp.Versioning.Mvc`:
|
||||
- Management API: `/umbraco/management/api/v{version}/*`
|
||||
- Delivery API: `/umbraco/delivery/api/v{version}/*`
|
||||
- OpenAPI/Swagger docs per version
|
||||
|
||||
### Known Limitations
|
||||
|
||||
1. **Circular Dependencies**: Avoided via `Lazy<T>` or event notifications
|
||||
2. **Multi-Server**: Requires shared Data Protection key ring and synchronized clocks (NTP)
|
||||
3. **Database Support**: SQL Server, SQLite
|
||||
|
||||
---
|
||||
|
||||
## Quick Reference
|
||||
|
||||
### Essential Commands
|
||||
|
||||
```bash
|
||||
# Build solution
|
||||
dotnet build
|
||||
|
||||
# Run all tests
|
||||
dotnet test
|
||||
|
||||
# Run specific test category
|
||||
dotnet test --filter "Category=Integration"
|
||||
|
||||
# Format code
|
||||
dotnet format
|
||||
|
||||
# Pack all projects
|
||||
dotnet pack -c Release
|
||||
```
|
||||
|
||||
### Key Projects
|
||||
|
||||
| Project | Type | Description |
|
||||
|---------|------|-------------|
|
||||
| **Umbraco.Core** | Library | Interface contracts and domain models |
|
||||
| **Umbraco.Infrastructure** | Library | Service implementations and data access |
|
||||
| **Umbraco.Web.UI** | Application | Main web application (Razor/MVC) |
|
||||
| **Umbraco.Cms.Api.Management** | Library | Management API (backoffice) |
|
||||
| **Umbraco.Cms.Api.Delivery** | Library | Delivery API (headless CMS) |
|
||||
| **Umbraco.Cms.Api.Common** | Library | Shared API infrastructure |
|
||||
| **Umbraco.PublishedCache.HybridCache** | Library | Published content caching |
|
||||
| **Umbraco.Examine.Lucene** | Library | Full-text search indexing |
|
||||
|
||||
### Important Files
|
||||
|
||||
- **Solution**: `umbraco.sln`
|
||||
- **Build Config**: `Directory.Build.props`, `Directory.Packages.props`
|
||||
- **Code Style**: `.editorconfig`, `.globalconfig`
|
||||
- **Documentation**: `/CLAUDE.md`, `/src/Umbraco.Core/CLAUDE.md`, `/src/Umbraco.Cms.Api.Common/CLAUDE.md`
|
||||
|
||||
### Project-Specific Documentation
|
||||
|
||||
For detailed information about individual projects, see their CLAUDE.md files:
|
||||
- **Core Architecture**: `/src/Umbraco.Core/CLAUDE.md` - Service contracts, notification patterns
|
||||
- **API Infrastructure**: `/src/Umbraco.Cms.Api.Common/CLAUDE.md` - OpenAPI, authentication, serialization
|
||||
|
||||
### Getting Help
|
||||
|
||||
- **Official Docs**: https://docs.umbraco.com/
|
||||
- **Contributing Guide**: `.github/CONTRIBUTING.md`
|
||||
- **Issues**: https://github.com/umbraco/Umbraco-CMS/issues
|
||||
- **Community**: https://forum.umbraco.com/
|
||||
- **Releases**: https://releases.umbraco.com/
|
||||
|
||||
---
|
||||
|
||||
**This repository follows a layered architecture with strict dependency rules. The Core defines contracts, Infrastructure implements them, and Web/APIs consume them. Each layer can be understood independently, but dependencies always flow inward toward Core.**
|
||||
@@ -1,7 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project>
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<Company>Umbraco HQ</Company>
|
||||
<Authors>Umbraco</Authors>
|
||||
<Copyright>Copyright © Umbraco $([System.DateTime]::Today.ToString('yyyy'))</Copyright>
|
||||
@@ -41,7 +41,7 @@
|
||||
<PropertyGroup>
|
||||
<GenerateCompatibilitySuppressionFile>false</GenerateCompatibilitySuppressionFile>
|
||||
<EnablePackageValidation>true</EnablePackageValidation>
|
||||
<PackageValidationBaselineVersion>17.0.0</PackageValidationBaselineVersion>
|
||||
<PackageValidationBaselineVersion>16.0.0</PackageValidationBaselineVersion>
|
||||
<EnableStrictModeForCompatibleFrameworksInPackage>true</EnableStrictModeForCompatibleFrameworksInPackage>
|
||||
<EnableStrictModeForCompatibleTfms>true</EnableStrictModeForCompatibleTfms>
|
||||
</PropertyGroup>
|
||||
|
||||
+59
-38
@@ -5,35 +5,36 @@
|
||||
</PropertyGroup>
|
||||
<!-- Global packages (private, build-time packages for all projects) -->
|
||||
<ItemGroup>
|
||||
<GlobalPackageReference Include="Nerdbank.GitVersioning" Version="3.9.50" />
|
||||
<GlobalPackageReference Include="Nerdbank.GitVersioning" Version="3.7.115" />
|
||||
<GlobalPackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.556" />
|
||||
<GlobalPackageReference Include="Umbraco.Code" Version="2.4.0" />
|
||||
<GlobalPackageReference Include="Umbraco.Code" Version="2.3.0" />
|
||||
<GlobalPackageReference Include="Umbraco.GitVersioning.Extensions" Version="0.2.0" />
|
||||
</ItemGroup>
|
||||
<!-- Microsoft packages -->
|
||||
<ItemGroup>
|
||||
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation" Version="10.0.0" />
|
||||
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="4.14.0" />
|
||||
<PackageVersion Include="Microsoft.Data.Sqlite" Version="10.0.0" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="10.0.0" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.SqlServer" Version="10.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Caching.Abstractions" Version="10.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="10.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Configuration.Abstractions" Version="10.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Configuration.Json" Version="10.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="10.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.FileProviders.Embedded" Version="10.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.FileProviders.Physical" Version="10.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="10.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Http" Version="10.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Identity.Core" Version="10.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Identity.Stores" Version="10.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Logging" Version="10.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Options" Version="10.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="10.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Options.DataAnnotations" Version="10.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Caching.Hybrid" Version="10.0.0" />
|
||||
<PackageVersion Include="System.Linq.Async" Version="7.0.0" />
|
||||
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation" Version="9.0.4" />
|
||||
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="4.13.0" />
|
||||
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="4.13.0" />
|
||||
<PackageVersion Include="Microsoft.CodeAnalysis.Workspaces.MSBuild" Version="4.13.0" />
|
||||
<PackageVersion Include="Microsoft.Data.Sqlite" Version="9.0.4" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="9.0.4" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.SqlServer" Version="9.0.4" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Caching.Abstractions" Version="9.0.4" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="9.0.4" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Configuration.Abstractions" Version="9.0.4" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Configuration.Json" Version="9.0.4" />
|
||||
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="9.0.4" />
|
||||
<PackageVersion Include="Microsoft.Extensions.FileProviders.Embedded" Version="9.0.4" />
|
||||
<PackageVersion Include="Microsoft.Extensions.FileProviders.Physical" Version="9.0.4" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="9.0.4" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Http" Version="9.0.4" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Identity.Core" Version="9.0.4" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Identity.Stores" Version="9.0.4" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Logging" Version="9.0.4" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Options" Version="9.0.4" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="9.0.4" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Options.DataAnnotations" Version="9.0.4" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Caching.Hybrid" Version="9.4.0" />
|
||||
</ItemGroup>
|
||||
<!-- Umbraco packages -->
|
||||
<ItemGroup>
|
||||
@@ -46,21 +47,21 @@
|
||||
<PackageVersion Include="Dazinator.Extensions.FileProviders" Version="2.0.0" />
|
||||
<PackageVersion Include="Examine" Version="3.7.1" />
|
||||
<PackageVersion Include="Examine.Core" Version="3.7.1" />
|
||||
<PackageVersion Include="HtmlAgilityPack" Version="1.12.4" />
|
||||
<PackageVersion Include="HtmlAgilityPack" Version="1.12.1" />
|
||||
<PackageVersion Include="JsonPatch.Net" Version="3.3.0" />
|
||||
<PackageVersion Include="K4os.Compression.LZ4" Version="1.3.8" />
|
||||
<PackageVersion Include="MailKit" Version="4.14.1" />
|
||||
<PackageVersion Include="MailKit" Version="4.11.0" />
|
||||
<PackageVersion Include="Markdown" Version="2.2.1" />
|
||||
<PackageVersion Include="MessagePack" Version="3.1.4" />
|
||||
<PackageVersion Include="MessagePack" Version="3.1.3" />
|
||||
<PackageVersion Include="MiniProfiler.AspNetCore.Mvc" Version="4.5.4" />
|
||||
<PackageVersion Include="MiniProfiler.Shared" Version="4.5.4" />
|
||||
<PackageVersion Include="ncrontab" Version="3.4.0" />
|
||||
<PackageVersion Include="NPoco" Version="6.1.0" />
|
||||
<PackageVersion Include="NPoco.SqlServer" Version="6.1.0" />
|
||||
<PackageVersion Include="OpenIddict.Abstractions" Version="7.2.0" />
|
||||
<PackageVersion Include="OpenIddict.AspNetCore" Version="7.2.0" />
|
||||
<PackageVersion Include="OpenIddict.EntityFrameworkCore" Version="7.2.0" />
|
||||
<PackageVersion Include="Serilog" Version="4.3.0" />
|
||||
<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.2.1" />
|
||||
<PackageVersion Include="OpenIddict.AspNetCore" Version="6.2.1" />
|
||||
<PackageVersion Include="OpenIddict.EntityFrameworkCore" Version="6.2.1" />
|
||||
<PackageVersion Include="Serilog" Version="4.2.0" />
|
||||
<PackageVersion Include="Serilog.AspNetCore" Version="9.0.0" />
|
||||
<PackageVersion Include="Serilog.Enrichers.Process" Version="3.0.0" />
|
||||
<PackageVersion Include="Serilog.Enrichers.Thread" Version="4.0.0" />
|
||||
@@ -70,13 +71,33 @@
|
||||
<PackageVersion Include="Serilog.Formatting.Compact.Reader" Version="4.0.0" />
|
||||
<PackageVersion Include="Serilog.Settings.Configuration" Version="9.0.0" />
|
||||
<PackageVersion Include="Serilog.Sinks.Async" Version="2.1.0" />
|
||||
<PackageVersion Include="Serilog.Sinks.File" Version="7.0.0" />
|
||||
<PackageVersion Include="Serilog.Sinks.File" Version="6.0.0" />
|
||||
<PackageVersion Include="Serilog.Sinks.Map" Version="2.0.0" />
|
||||
<PackageVersion Include="SixLabors.ImageSharp" Version="3.1.12" />
|
||||
<PackageVersion Include="SixLabors.ImageSharp.Web" Version="3.2.0" />
|
||||
<PackageVersion Include="Swashbuckle.AspNetCore" Version="10.0.1" />
|
||||
<PackageVersion Include="SixLabors.ImageSharp" Version="3.1.11" />
|
||||
<PackageVersion Include="SixLabors.ImageSharp.Web" Version="3.1.5" />
|
||||
<PackageVersion Include="Swashbuckle.AspNetCore" Version="8.1.1" />
|
||||
</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 -->
|
||||
<PackageVersion Include="Azure.Identity" Version="1.13.2" />
|
||||
<!-- Microsoft.EntityFrameworkCore.SqlServer brings in a vulnerable version of System.Runtime.Caching -->
|
||||
<PackageVersion Include="System.Runtime.Caching" Version="9.0.4" />
|
||||
<!-- 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.4" />
|
||||
<!-- Dazinator.Extensions.FileProviders and MiniProfiler.AspNetCore.Mvc brings 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.8.0" />
|
||||
<!-- 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.4" />
|
||||
<!-- NPoco.SqlServer brings in a vulnerable version of Microsoft.Data.SqlClient -->
|
||||
<PackageVersion Include="Microsoft.Data.SqlClient" Version="6.0.1" />
|
||||
<!-- Examine.Lucene brings in a vulnerable version of Lucene.Net.Replicator -->
|
||||
<PackageVersion Include="Lucene.Net.Replicator" Version="4.8.0-beta00017" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
@@ -215,7 +215,7 @@ stages:
|
||||
inputs:
|
||||
targetType: inline
|
||||
script: |
|
||||
dotnet tool update -g docfx
|
||||
choco install docfx --version=2.59.4 -y
|
||||
if ($lastexitcode -ne 0){
|
||||
throw ("Error installing DocFX")
|
||||
}
|
||||
@@ -224,14 +224,10 @@ stages:
|
||||
inputs:
|
||||
targetType: inline
|
||||
script: |
|
||||
# Using .cs files directly instead of .csproj to avoid MSBuild/NBGV hangs on Azure DevOps
|
||||
# See: https://github.com/dotnet/docfx/issues/10144
|
||||
Write-Host "Starting docfx metadata generation from source files..."
|
||||
docfx metadata "$(Build.SourcesDirectory)/build/csharp-docs/docfx.json"
|
||||
if ($lastexitcode -ne 0){
|
||||
throw ("Error generating metadata.")
|
||||
}
|
||||
timeoutInMinutes: 30
|
||||
- task: PowerShell@2
|
||||
displayName: Generate documentation
|
||||
inputs:
|
||||
@@ -369,8 +365,8 @@ stages:
|
||||
testFilter: "(FullyQualifiedName~Umbraco.Infrastructure.Service)"
|
||||
LinuxPart3Of3:
|
||||
vmImage: "ubuntu-latest"
|
||||
# Filter tests that are not part of the Umbraco.Infrastructure and ManagementApi namespace. So this will run all tests that are not part of the Umbraco.Infrastructure and ManagementApi namespace
|
||||
testFilter: "(FullyQualifiedName!~Umbraco.Infrastructure) & (FullyQualifiedName!~ManagementApi)"
|
||||
# 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
|
||||
@@ -381,8 +377,8 @@ stages:
|
||||
testFilter: "(FullyQualifiedName~Umbraco.Infrastructure.Service)"
|
||||
macOSPart3Of3:
|
||||
vmImage: "macOS-latest"
|
||||
# Filter tests that are not part of the Umbraco.Infrastructure and ManagementApi namespace. So this will run all tests that are not part of the Umbraco.Infrastructure and the ManagementApi namespace
|
||||
testFilter: "(FullyQualifiedName!~Umbraco.Infrastructure) & (FullyQualifiedName!~ManagementApi)"
|
||||
# Filter tests that are not part of the Umbraco.Infrastructure namespace.
|
||||
testFilter: "(FullyQualifiedName!~Umbraco.Infrastructure)"
|
||||
pool:
|
||||
vmImage: $(vmImage)
|
||||
variables:
|
||||
@@ -445,8 +441,8 @@ stages:
|
||||
vmImage: "windows-latest"
|
||||
Tests__Database__DatabaseType: LocalDb
|
||||
Tests__Database__SQLServerMasterConnectionString: N/A
|
||||
# Filter tests that are not part of the Umbraco.Infrastructure and ManagementApi namespace. So this will run all tests that are not part of the Umbraco.Infrastructure and ManagementApi namespace
|
||||
testFilter: "(FullyQualifiedName!~Umbraco.Infrastructure) & (FullyQualifiedName!~ManagementApi)"
|
||||
# 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"
|
||||
SA_PASSWORD: UmbracoIntegration123!
|
||||
@@ -466,8 +462,8 @@ stages:
|
||||
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 and ManagementApi namespace. So this will run all tests that are not part of the Umbraco.Infrastructure and ManagementApi namespace
|
||||
testFilter: "(FullyQualifiedName!~Umbraco.Infrastructure) & (FullyQualifiedName!~ManagementApi)"
|
||||
# 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)"
|
||||
pool:
|
||||
vmImage: $(vmImage)
|
||||
steps:
|
||||
|
||||
@@ -5,22 +5,20 @@
|
||||
{
|
||||
"src": "../../src",
|
||||
"files": [
|
||||
"**/*.cs"
|
||||
"**/*.csproj"
|
||||
],
|
||||
"exclude": [
|
||||
"**/obj/**",
|
||||
"**/bin/**",
|
||||
"**/Umbraco.Web.UI/**",
|
||||
"**/Umbraco.Cms.StaticAssets/**",
|
||||
"**/Umbraco.Cms/**",
|
||||
"**/Umbraco.Cms.Targets/**"
|
||||
"**/Umbraco.Web.csproj",
|
||||
"**/Umbraco.Web.UI.csproj",
|
||||
"**/Umbraco.Cms.StaticAssets.csproj",
|
||||
"**/JsonSchema.csproj"
|
||||
]
|
||||
}
|
||||
],
|
||||
"dest": "api",
|
||||
"filter": "docfx.filter.yml",
|
||||
"disableGitFeatures": true,
|
||||
"maxParallelism": 1
|
||||
"filter": "docfx.filter.yml"
|
||||
}
|
||||
],
|
||||
"build": {
|
||||
|
||||
@@ -4,13 +4,11 @@ pr: none
|
||||
trigger: none
|
||||
|
||||
schedules:
|
||||
- cron: '0 0 * * *'
|
||||
displayName: Daily midnight build
|
||||
- cron: '0 6 * * *'
|
||||
displayName: Daily 6 AM build (v16/dev)
|
||||
branches:
|
||||
include:
|
||||
- v15/dev
|
||||
- v16/dev
|
||||
- main
|
||||
|
||||
parameters:
|
||||
- name: skipIntegrationTests
|
||||
@@ -18,8 +16,8 @@ parameters:
|
||||
type: boolean
|
||||
default: false
|
||||
|
||||
- name: skipDifferentAppSettingsAcceptanceTests
|
||||
displayName: Skip acceptance tests with different app settings
|
||||
- name: differentAppSettingsAcceptanceTests
|
||||
displayName: Run acceptance tests with different app settings
|
||||
type: boolean
|
||||
default: false
|
||||
|
||||
@@ -123,39 +121,31 @@ stages:
|
||||
matrix:
|
||||
# Windows:
|
||||
# vmImage: 'windows-latest'
|
||||
# We split the tests into 4 parts for each OS to reduce the time it takes to run them on the pipeline
|
||||
LinuxPart1Of4:
|
||||
# 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)"
|
||||
LinuxPart2Of4:
|
||||
LinuxPart2Of3:
|
||||
vmImage: "ubuntu-latest"
|
||||
# Filter tests that are part of the Umbraco.Infrastructure.Service namespace
|
||||
testFilter: "(FullyQualifiedName~Umbraco.Infrastructure.Service)"
|
||||
LinuxPart3Of4:
|
||||
LinuxPart3Of3:
|
||||
vmImage: "ubuntu-latest"
|
||||
# Filter tests that are not part of the Umbraco.Infrastructure and ManagementApi namespace. So this will run all tests that are not part of the Umbraco.Infrastructure namespace
|
||||
testFilter: "(FullyQualifiedName!~Umbraco.Infrastructure) & (FullyQualifiedName!~ManagementApi)"
|
||||
LinuxPart4Of4:
|
||||
vmImage: "ubuntu-latest"
|
||||
# Filter tests that are part of the ManagementApi namespace
|
||||
testFilter: "(FullyQualifiedName~ManagementApi)"
|
||||
macOSPart1Of4:
|
||||
# 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)"
|
||||
macOSPart2Of4:
|
||||
macOSPart2Of3:
|
||||
vmImage: "macOS-latest"
|
||||
# Filter tests that are part of the Umbraco.Infrastructure.Service namespace
|
||||
testFilter: "(FullyQualifiedName~Umbraco.Infrastructure.Service)"
|
||||
macOSPart3Of4:
|
||||
macOSPart3Of3:
|
||||
vmImage: "macOS-latest"
|
||||
# Filter tests that are not part of the Umbraco.Infrastructure and ManagementApi namespace.
|
||||
testFilter: "(FullyQualifiedName!~Umbraco.Infrastructure) & (FullyQualifiedName!~ManagementApi)"
|
||||
macOSPart4Of4:
|
||||
vmImage: "macOS-latest"
|
||||
# Filter tests that are part of the ManagementApi namespace.
|
||||
testFilter: "(FullyQualifiedName~ManagementApi)"
|
||||
# Filter tests that are not part of the Umbraco.Infrastructure namespace.
|
||||
testFilter: "(FullyQualifiedName!~Umbraco.Infrastructure)"
|
||||
pool:
|
||||
vmImage: $(vmImage)
|
||||
variables:
|
||||
@@ -195,55 +185,43 @@ stages:
|
||||
SA_PASSWORD: UmbracoAcceptance123!
|
||||
strategy:
|
||||
matrix:
|
||||
# We split the tests into 4 parts for each OS to reduce the time it takes to run them on the pipeline
|
||||
WindowsPart1Of4:
|
||||
# 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"
|
||||
Tests__Database__DatabaseType: LocalDb
|
||||
Tests__Database__SQLServerMasterConnectionString: N/A
|
||||
# Filter tests that are part of the Umbraco.Infrastructure namespace but not part of the Umbraco.Infrastructure.Service namespace
|
||||
testFilter: "(FullyQualifiedName~Umbraco.Infrastructure) & (FullyQualifiedName!~Umbraco.Infrastructure.Service)"
|
||||
WindowsPart2Of4:
|
||||
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)"
|
||||
WindowsPart3Of4:
|
||||
WindowsPart3Of3:
|
||||
vmImage: "windows-latest"
|
||||
Tests__Database__DatabaseType: LocalDb
|
||||
Tests__Database__SQLServerMasterConnectionString: N/A
|
||||
# Filter tests that are not part of the Umbraco.Infrastructure and ManagementApi namespace.
|
||||
testFilter: "(FullyQualifiedName!~Umbraco.Infrastructure) & (FullyQualifiedName!~ManagementApi)"
|
||||
WindowsPart4Of4:
|
||||
vmImage: "windows-latest"
|
||||
Tests__Database__DatabaseType: LocalDb
|
||||
Tests__Database__SQLServerMasterConnectionString: N/A
|
||||
# Filter tests that are part of the ManagementApi namespace.
|
||||
testFilter: "(FullyQualifiedName~ManagementApi)"
|
||||
LinuxPart1Of4:
|
||||
# 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"
|
||||
Tests__Database__DatabaseType: SqlServer
|
||||
Tests__Database__SQLServerMasterConnectionString: "Server=(local);User Id=sa;Password=$(SA_PASSWORD);Encrypt=True;TrustServerCertificate=True"
|
||||
# Filter tests that are part of the Umbraco.Infrastructure namespace but not part of the Umbraco.Infrastructure.Service namespace
|
||||
testFilter: "(FullyQualifiedName~Umbraco.Infrastructure) & (FullyQualifiedName!~Umbraco.Infrastructure.Service)"
|
||||
LinuxPart2Of4:
|
||||
LinuxPart2Of3:
|
||||
vmImage: "ubuntu-latest"
|
||||
Tests__Database__DatabaseType: SqlServer
|
||||
Tests__Database__SQLServerMasterConnectionString: "Server=(local);User Id=sa;Password=$(SA_PASSWORD);Encrypt=True;TrustServerCertificate=True"
|
||||
# Filter tests that are part of the Umbraco.Infrastructure.Service namespace
|
||||
testFilter: "(FullyQualifiedName~Umbraco.Infrastructure.Service)"
|
||||
LinuxPart3Of4:
|
||||
LinuxPart3Of3:
|
||||
vmImage: "ubuntu-latest"
|
||||
Tests__Database__DatabaseType: SqlServer
|
||||
Tests__Database__SQLServerMasterConnectionString: "Server=(local);User Id=sa;Password=$(SA_PASSWORD);Encrypt=True;TrustServerCertificate=True"
|
||||
# Filter tests that are not part of the Umbraco.Infrastructure and ManagementApi namespace.
|
||||
testFilter: "(FullyQualifiedName!~Umbraco.Infrastructure) & (FullyQualifiedName!~ManagementApi)"
|
||||
LinuxPart4Of4:
|
||||
vmImage: "ubuntu-latest"
|
||||
Tests__Database__DatabaseType: SqlServer
|
||||
Tests__Database__SQLServerMasterConnectionString: "Server=(local);User Id=sa;Password=$(SA_PASSWORD);Encrypt=True;TrustServerCertificate=True"
|
||||
# Filter tests that are part of the ManagementApi namespace.
|
||||
testFilter: "(FullyQualifiedName~ManagementApi)"
|
||||
# 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)"
|
||||
pool:
|
||||
vmImage: $(vmImage)
|
||||
steps:
|
||||
@@ -315,7 +293,8 @@ stages:
|
||||
|
||||
- stage: DefaultConfigE2E
|
||||
displayName: Default Config E2E Tests
|
||||
dependsOn: Build
|
||||
dependsOn: Integration
|
||||
condition: always()
|
||||
variables:
|
||||
npm_config_cache: $(Pipeline.Workspace)/.npm_e2e
|
||||
# Enable console logging in Release mode
|
||||
@@ -443,15 +422,15 @@ stages:
|
||||
vmImage: "ubuntu-latest"
|
||||
CONNECTIONSTRINGS__UMBRACODBDSN: "Server=(local);Database=Umbraco;User Id=sa;Password=$(SA_PASSWORD);Encrypt=True;TrustServerCertificate=True"
|
||||
WindowsPart1Of3:
|
||||
testCommand: "npm run testWindows -- --shard=1/3"
|
||||
testCommand: "npm run test -- --shard=1/3"
|
||||
testFolder: "DefaultConfig"
|
||||
vmImage: "windows-latest"
|
||||
WindowsPart2Of3:
|
||||
testCommand: "npm run testWindows -- --shard=2/3"
|
||||
testCommand: "npm run test -- --shard=2/3"
|
||||
testFolder: "DefaultConfig"
|
||||
vmImage: "windows-latest"
|
||||
WindowsPart3Of3:
|
||||
testCommand: "npm run testWindows -- --shard=3/3"
|
||||
testCommand: "npm run test -- --shard=3/3"
|
||||
testFolder: "DefaultConfig"
|
||||
vmImage: "windows-latest"
|
||||
pool:
|
||||
@@ -496,7 +475,8 @@ stages:
|
||||
|
||||
- stage: AdditionalConfigE2E
|
||||
displayName: Additional Config E2E Tests
|
||||
dependsOn: Build
|
||||
dependsOn: DefaultConfigE2E
|
||||
condition: always()
|
||||
variables:
|
||||
npm_config_cache: $(Pipeline.Workspace)/.npm_e2e
|
||||
ASPNETCORE_URLS: https://localhost:44331
|
||||
@@ -505,7 +485,7 @@ stages:
|
||||
jobs:
|
||||
- job:
|
||||
displayName: E2E Tests with Different App settings (SQL Server)
|
||||
condition: ${{ eq(parameters.skipDifferentAppSettingsAcceptanceTests, false) }}
|
||||
condition: ${{ or(eq(parameters.differentAppSettingsAcceptanceTests, true), eq(parameters.skipDefaultConfigAcceptanceTests, true)) }}
|
||||
timeoutInMinutes: 180
|
||||
variables:
|
||||
SA_PASSWORD: UmbracoAcceptance123!
|
||||
@@ -564,32 +544,6 @@ stages:
|
||||
CONNECTIONSTRINGS__UMBRACODBDSN: Server=(local);Database=Umbraco;User Id=sa;Password=$(SA_PASSWORD);Encrypt=True;TrustServerCertificate=True
|
||||
CONNECTIONSTRINGS__UMBRACODBDSN_PROVIDERNAME: Microsoft.Data.SqlClient
|
||||
additionalEnvironmentVariables: false
|
||||
# EntityDataPicker
|
||||
WindowsEntityDataPicker:
|
||||
vmImage: "windows-latest"
|
||||
testFolder: "EntityDataPicker"
|
||||
port: ''
|
||||
testCommand: "npx playwright test --project=entityDataPicker"
|
||||
CONNECTIONSTRINGS__UMBRACODBDSN: Data Source=(localdb)\MSSQLLocalDB;AttachDbFilename=|DataDirectory|\Umbraco.mdf;Integrated Security=True
|
||||
CONNECTIONSTRINGS__UMBRACODBDSN_PROVIDERNAME: Microsoft.Data.SqlClient
|
||||
additionalEnvironmentVariables: false
|
||||
LinuxEntityDataPicker:
|
||||
vmImage: "ubuntu-latest"
|
||||
testFolder: "EntityDataPicker"
|
||||
port: ''
|
||||
testCommand: "npx playwright test --project=entityDataPicker"
|
||||
CONNECTIONSTRINGS__UMBRACODBDSN: Server=(local);Database=Umbraco;User Id=sa;Password=$(SA_PASSWORD);Encrypt=True;TrustServerCertificate=True
|
||||
CONNECTIONSTRINGS__UMBRACODBDSN_PROVIDERNAME: Microsoft.Data.SqlClient
|
||||
additionalEnvironmentVariables: false
|
||||
# SMTP
|
||||
LinuxSMTP:
|
||||
vmImage: "ubuntu-latest"
|
||||
testFolder: "SMTP"
|
||||
port: ''
|
||||
testCommand: "npx playwright test --project=smtp"
|
||||
CONNECTIONSTRINGS__UMBRACODBDSN: Server=(local);Database=Umbraco;User Id=sa;Password=$(SA_PASSWORD);Encrypt=True;TrustServerCertificate=True
|
||||
CONNECTIONSTRINGS__UMBRACODBDSN_PROVIDERNAME: Microsoft.Data.SqlClient
|
||||
additionalEnvironmentVariables: false
|
||||
pool:
|
||||
vmImage: $(vmImage)
|
||||
steps:
|
||||
@@ -667,23 +621,6 @@ stages:
|
||||
AZUREADB2CCLIENTID: $(AZUREB2CCLIENTID)
|
||||
AZUREADB2CCLIENTSECRET: $(AZUREB2CCLIENTSECRET)
|
||||
|
||||
# Start SMTP4dev via Docker for SMTP tests
|
||||
- bash: |
|
||||
echo "Starting SMTP4dev container..."
|
||||
docker run -d --name smtp4dev -p 5000:80 -p 25:25 rnwood/smtp4dev
|
||||
|
||||
echo "Waiting for SMTP4dev to be ready..."
|
||||
for i in {1..30}; do
|
||||
if curl -s http://localhost:5000/api/messages > /dev/null; then
|
||||
echo "SMTP4dev is ready"
|
||||
break
|
||||
fi
|
||||
echo "Attempt $i: Waiting for SMTP4dev..."
|
||||
sleep 2
|
||||
done
|
||||
displayName: Start SMTP4dev Docker container (Linux)
|
||||
condition: and(succeeded(), eq(variables['Agent.OS'], 'Linux'), contains(variables['testFolder'], 'SMTP'))
|
||||
|
||||
# Run tests Template
|
||||
- template: nightly-E2E-run-tests-template.yml
|
||||
parameters:
|
||||
@@ -693,14 +630,6 @@ stages:
|
||||
AZUREB2CTESTUSEREMAIL: $(AZUREB2CTESTUSEREMAIL)
|
||||
AZUREB2CTESTUSERPASSWORD: $(AZUREB2CTESTUSERPASSWORD)
|
||||
DatabaseType: ${{ variables.DatabaseType }}
|
||||
|
||||
# Stop SMTP4dev container
|
||||
- bash: |
|
||||
echo "Stopping SMTP4dev container..."
|
||||
docker stop smtp4dev
|
||||
docker rm smtp4dev
|
||||
displayName: Stop SMTP4dev Docker container
|
||||
condition: and(succeeded(), eq(variables['Agent.OS'], 'Linux'), contains(variables['testFolder'], 'SMTP'))
|
||||
|
||||
- stage: NotifySlackBot
|
||||
displayName: Notify Slack on Failure
|
||||
|
||||
@@ -13,12 +13,12 @@ jobs:
|
||||
- checkout: none
|
||||
|
||||
- bash: |
|
||||
project_id=$(curl --no-progress-meter -H "X-Api-Key: $(DT_API_KEY)" "$(DT_API_URI)/api/v1/project/lookup?name=${{ parameters.projectName }}&version=${{ parameters.umbracoVersion }}" | jq -r '.uuid')
|
||||
project_id=$(curl --no-progress-meter -H "X-Api-Key: $(DT_API_KEY)" "$(DT_API_URL)/v1/project/lookup?name=${{ parameters.projectName }}&version=${{ parameters.umbracoVersion }}" | jq -r '.uuid')
|
||||
if [ "$project_id" != "null" ] && [ -n "$project_id" ]; then
|
||||
echo "Project '${{ parameters.projectName }}' with version '${{ parameters.umbracoVersion }}' already exists (ID: $project_id)."
|
||||
else
|
||||
project_id=$(curl --no-progress-meter \
|
||||
-X PUT "$(DT_API_URI)/api/v1/project" \
|
||||
-X PUT "$(DT_API_URL)/v1/project" \
|
||||
-H "X-Api-Key: $(DT_API_KEY)" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"name": "${{ parameters.projectName }}", "version": "${{ parameters.umbracoVersion }}", "collectionLogic": "AGGREGATE_DIRECT_CHILDREN"}' \
|
||||
@@ -42,14 +42,15 @@ jobs:
|
||||
artifact: ${{ project.artifact }}
|
||||
displayName: Download ${{ project.artifact }} artifact
|
||||
|
||||
- task: upload-bom-dtrack@1
|
||||
inputs:
|
||||
dtrackURI: $(DT_API_URI)
|
||||
dtrackAPIKey: $(DT_API_KEY)
|
||||
dtrackProjAutoCreate: true
|
||||
dtrackProjName: '${{ parameters.projectName }}-${{ project.name }}'
|
||||
dtrackProjVersion: ${{ parameters.umbracoVersion }}
|
||||
dtrackParentProjName: ${{ parameters.projectName }}
|
||||
dtrackParentProjVersion: ${{ parameters.umbracoVersion }}
|
||||
bomFilePath: '$(Pipeline.Workspace)/${{ project.artifact }}/${{ project.bomFilePath }}'
|
||||
- script: |
|
||||
curl --no-progress-meter --fail-with-body \
|
||||
-X POST "$(DT_API_URL)/v1/bom" \
|
||||
-H "X-Api-Key: $(DT_API_KEY)" \
|
||||
-H "Content-Type: multipart/form-data" \
|
||||
-F "autoCreate=true" \
|
||||
-F "projectName=${{ parameters.projectName }}-${{ project.name }}" \
|
||||
-F "projectVersion=${{ parameters.umbracoVersion }}" \
|
||||
-F "parentName=${{ parameters.projectName }}" \
|
||||
-F "parentVersion=${{ parameters.umbracoVersion }}" \
|
||||
-F "bom=@$(Pipeline.Workspace)/${{ project.artifact }}/${{ project.bomFilePath }}"
|
||||
displayName: Upload ${{ project.name }} BOM to Dependency Track
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
{
|
||||
"url": "https://context7.com/umbraco/umbraco-cms",
|
||||
"public_key": "pk_GTIgsrGAQiHNxCirZBDIM"
|
||||
}
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"sdk": {
|
||||
"version": "10.0.100",
|
||||
"version": "9.0.306",
|
||||
"rollForward": "latestFeature",
|
||||
"allowPrerelease": false
|
||||
}
|
||||
|
||||
@@ -1,371 +0,0 @@
|
||||
# Umbraco.Cms.Api.Common
|
||||
|
||||
Shared infrastructure for Umbraco CMS REST APIs (Management and Delivery).
|
||||
|
||||
---
|
||||
|
||||
## 1. Architecture
|
||||
|
||||
**Type**: Class Library (NuGet Package)
|
||||
**Target Framework**: .NET 10.0
|
||||
**Purpose**: Common API infrastructure - OpenAPI/Swagger, JSON serialization, OpenIddict authentication, problem details
|
||||
|
||||
### Key Technologies
|
||||
|
||||
- **ASP.NET Core** - Web framework
|
||||
- **Swashbuckle** - OpenAPI/Swagger documentation generation
|
||||
- **OpenIddict** - OAuth 2.0/OpenID Connect authentication
|
||||
- **Asp.Versioning** - API versioning
|
||||
- **System.Text.Json** - Polymorphic JSON serialization
|
||||
|
||||
### Dependencies
|
||||
|
||||
- `Umbraco.Core` - Domain models and service contracts
|
||||
- `Umbraco.Web.Common` - Web functionality
|
||||
|
||||
### Project Structure (46 files)
|
||||
|
||||
```
|
||||
Umbraco.Cms.Api.Common/
|
||||
├── OpenApi/ # Schema/Operation ID handlers for Swagger
|
||||
│ ├── SchemaIdHandler.cs # Generates schema IDs (e.g., "PagedUserModel")
|
||||
│ ├── OperationIdHandler.cs # Generates operation IDs
|
||||
│ └── SubTypesHandler.cs # Polymorphism support
|
||||
├── Serialization/ # JSON type resolution
|
||||
│ └── UmbracoJsonTypeInfoResolver.cs
|
||||
├── Configuration/ # Options configuration
|
||||
│ ├── ConfigureUmbracoSwaggerGenOptions.cs
|
||||
│ └── ConfigureOpenIddict.cs
|
||||
├── DependencyInjection/ # Service registration
|
||||
│ ├── UmbracoBuilderApiExtensions.cs
|
||||
│ └── UmbracoBuilderAuthExtensions.cs
|
||||
├── Builders/ # RFC 7807 problem details
|
||||
│ └── ProblemDetailsBuilder.cs
|
||||
├── ViewModels/Pagination/ # Common DTOs
|
||||
└── Security/ # Auth paths and handlers
|
||||
```
|
||||
|
||||
### Design Patterns
|
||||
|
||||
1. **Strategy Pattern** - `ISchemaIdHandler`, `IOperationIdHandler` (extensible via inheritance)
|
||||
2. **Builder Pattern** - `ProblemDetailsBuilder` for fluent error responses
|
||||
3. **Options Pattern** - All configuration via `IConfigureOptions<T>`
|
||||
|
||||
---
|
||||
|
||||
## 2. Commands
|
||||
|
||||
See "Quick Reference" section at bottom for common commands.
|
||||
|
||||
---
|
||||
|
||||
## 3. Key Patterns
|
||||
|
||||
### Virtual Handlers for Extensibility
|
||||
|
||||
Handlers are intentionally virtual to allow consuming APIs to override:
|
||||
|
||||
```csharp
|
||||
// NOTE: Left unsealed on purpose, so it is extendable.
|
||||
public class SchemaIdHandler : ISchemaIdHandler
|
||||
{
|
||||
public virtual bool CanHandle(Type type) { }
|
||||
public virtual string Handle(Type type) { }
|
||||
}
|
||||
```
|
||||
|
||||
**Why**: Management and Delivery APIs can customize schema/operation ID generation.
|
||||
|
||||
### Schema ID Sanitization (OpenApi/SchemaIdHandler.cs:24-29, 32)
|
||||
|
||||
```csharp
|
||||
// Add "Model" suffix to avoid TypeScript name clashes (lines 24-29)
|
||||
if (name.EndsWith("Model") == false)
|
||||
{
|
||||
// because some models names clash with common classes in TypeScript (i.e. Document),
|
||||
// we need to add a "Model" postfix to all models
|
||||
name = $"{name}Model";
|
||||
}
|
||||
|
||||
// Remove invalid characters to prevent OpenAPI generation errors (line 32)
|
||||
return Regex.Replace(name, @"[^\w]", string.Empty);
|
||||
```
|
||||
|
||||
### Polymorphic Deserialization (Serialization/UmbracoJsonTypeInfoResolver.cs:29-35)
|
||||
|
||||
```csharp
|
||||
// IMPORTANT: do NOT return an empty enumerable here. it will cause nullability to fail on reference
|
||||
// properties, because "$ref" does not mix and match well with "nullable" in OpenAPI.
|
||||
if (type.IsInterface is false)
|
||||
{
|
||||
return new[] { type };
|
||||
}
|
||||
```
|
||||
|
||||
**Why**: Interfaces must return concrete types to avoid OpenAPI schema conflicts.
|
||||
|
||||
---
|
||||
|
||||
## 4. Testing
|
||||
|
||||
**Location**: No direct tests - tested via integration tests in consuming APIs
|
||||
|
||||
**How to test changes**:
|
||||
```bash
|
||||
# Run integration tests that exercise this library
|
||||
dotnet test tests/Umbraco.Tests.Integration/
|
||||
|
||||
# Verify OpenAPI generation
|
||||
# 1. Run Management API
|
||||
# 2. Navigate to /umbraco/swagger/
|
||||
# 3. Check schema IDs and operation IDs
|
||||
```
|
||||
|
||||
**Focus areas when testing**:
|
||||
- OpenAPI document generation (schema IDs, operation IDs)
|
||||
- Polymorphic JSON serialization/deserialization
|
||||
- OpenIddict authentication flow
|
||||
- Problem details formatting
|
||||
|
||||
---
|
||||
|
||||
## 5. OpenIddict Authentication
|
||||
|
||||
### Key Configuration (DependencyInjection/UmbracoBuilderAuthExtensions.cs)
|
||||
|
||||
**Reference Tokens over JWT** (line 76-80):
|
||||
```csharp
|
||||
// Enable reference tokens
|
||||
// - see https://documentation.openiddict.com/configuration/token-storage.html
|
||||
options
|
||||
.UseReferenceAccessTokens()
|
||||
.UseReferenceRefreshTokens();
|
||||
```
|
||||
|
||||
**Why**: More secure (revocable), better for load balancing, uses ASP.NET Core Data Protection.
|
||||
|
||||
**Token Lifetime** (line 88-91):
|
||||
```csharp
|
||||
// Make the access token lifetime 25% of the refresh token lifetime
|
||||
options.SetAccessTokenLifetime(new TimeSpan(timeOut.Ticks / 4));
|
||||
options.SetRefreshTokenLifetime(timeOut);
|
||||
```
|
||||
|
||||
**PKCE Required** (line 59-63):
|
||||
```csharp
|
||||
// Enable authorization code flow with PKCE
|
||||
options
|
||||
.AllowAuthorizationCodeFlow()
|
||||
.RequireProofKeyForCodeExchange()
|
||||
.AllowRefreshTokenFlow();
|
||||
```
|
||||
|
||||
**Endpoints**: Backoffice `/umbraco/management/api/v1/security/*`, Member `/umbraco/member/api/v1/security/*`
|
||||
|
||||
### Secure Cookie-Based Token Storage (v17+)
|
||||
|
||||
**Implementation** (DependencyInjection/HideBackOfficeTokensHandler.cs):
|
||||
|
||||
Back-office tokens are hidden from client-side JavaScript via HTTP-only cookies:
|
||||
|
||||
```csharp
|
||||
private const string AccessTokenCookieKey = "__Host-umbAccessToken";
|
||||
private const string RefreshTokenCookieKey = "__Host-umbRefreshToken";
|
||||
|
||||
// Tokens are encrypted via Data Protection and stored in cookies
|
||||
SetCookie(httpContext, AccessTokenCookieKey, context.Response.AccessToken);
|
||||
context.Response.AccessToken = "[redacted]"; // Client sees redacted value
|
||||
```
|
||||
|
||||
**Key Security Features** (lines 143-165): `HttpOnly`, `IsEssential`, `Path="/"`, `Secure` (HTTPS), `__Host-` prefix
|
||||
|
||||
**Configuration**: `BackOfficeTokenCookieSettings.Enabled` (default: true in v17+)
|
||||
|
||||
**Implications**: Client-side cannot access tokens; encrypted with Data Protection; load balancing needs shared key ring; API requests need `credentials: include`
|
||||
|
||||
---
|
||||
|
||||
## 6. Common Issues & Edge Cases
|
||||
|
||||
### Polymorphic Deserialization Requires `$type`
|
||||
|
||||
**Issue**: Deserializing to an interface without `$type` discriminator fails.
|
||||
|
||||
**Handled in** (Json/NamedSystemTextJsonInputFormatter.cs:24-29):
|
||||
```csharp
|
||||
catch (NotSupportedException exception)
|
||||
{
|
||||
// This happens when trying to deserialize to an interface, without sending the $type as part of the request
|
||||
context.ModelState.TryAddModelException(string.Empty, new InputFormatterException(exception.Message, exception));
|
||||
return await InputFormatterResult.FailureAsync();
|
||||
}
|
||||
```
|
||||
|
||||
**Solution**: Clients must include `$type` property for interface types, or use concrete types.
|
||||
|
||||
### Schema ID Collisions with TypeScript
|
||||
|
||||
**Issue**: Type names like `Document` clash with TypeScript built-ins.
|
||||
|
||||
**Solution**: Add "Model" suffix (OpenApi/SchemaIdHandler.cs:24-29)
|
||||
|
||||
### Generic Type Handling
|
||||
|
||||
**Issue**: `PagedViewModel<T>` needs flattened schema name.
|
||||
|
||||
**Solution** (OpenApi/SchemaIdHandler.cs:41-50):
|
||||
```csharp
|
||||
private string HandleGenerics(string name, Type type)
|
||||
{
|
||||
if (!type.IsGenericType)
|
||||
return name;
|
||||
|
||||
// use attribute custom name or append the generic type names
|
||||
// turns "PagedViewModel<RelationItemViewModel>" into "PagedRelationItem"
|
||||
return $"{name}{string.Join(string.Empty, type.GenericTypeArguments.Select(SanitizedTypeName))}";
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. Extending This Library
|
||||
|
||||
### Adding a Custom OpenAPI Handler
|
||||
|
||||
1. **Implement interface**:
|
||||
```csharp
|
||||
public class MySchemaIdHandler : SchemaIdHandler
|
||||
{
|
||||
public override bool CanHandle(Type type)
|
||||
=> type.Namespace?.StartsWith("MyProject") is true;
|
||||
|
||||
public override string Handle(Type type)
|
||||
=> $"My{base.Handle(type)}";
|
||||
}
|
||||
```
|
||||
|
||||
2. **Register in consuming API**:
|
||||
```csharp
|
||||
builder.Services.AddSingleton<ISchemaIdHandler, MySchemaIdHandler>();
|
||||
```
|
||||
|
||||
**Note**: Handlers registered later take precedence in the selector.
|
||||
|
||||
### Customizing Problem Details
|
||||
|
||||
```csharp
|
||||
var problemDetails = new ProblemDetailsBuilder()
|
||||
.WithTitle("Validation Failed")
|
||||
.WithDetail("The request contains errors")
|
||||
.WithType("ValidationError")
|
||||
.WithOperationStatus(MyOperationStatus.ValidationFailed)
|
||||
.WithRequestModelErrors(errors)
|
||||
.Build();
|
||||
|
||||
return BadRequest(problemDetails);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. Project-Specific Notes
|
||||
|
||||
### Why Virtual Handlers?
|
||||
|
||||
**Decision**: Make `SchemaIdHandler`, `OperationIdHandler`, etc. virtual.
|
||||
|
||||
**Why**: Management API and Delivery API have different schema ID requirements. Virtual methods allow override without rewriting the entire handler.
|
||||
|
||||
**Example**: Management API might prefix all schemas with "Management", Delivery API with "Delivery".
|
||||
|
||||
### Performance: Subtype Caching
|
||||
|
||||
**Why**: Cache discovered subtypes (UmbracoJsonTypeInfoResolver.cs:14) to avoid expensive reflection calls
|
||||
|
||||
### Known Limitations
|
||||
|
||||
1. **Polymorphic Deserialization**:
|
||||
- Requires `$type` discriminator in JSON for interfaces
|
||||
- Only discovers types in Umbraco namespaces
|
||||
- Not all .NET types are discoverable
|
||||
|
||||
2. **OpenAPI Schema Generation**:
|
||||
- Generic types are flattened (e.g., `PagedViewModel<T>` → `PagedTModel`)
|
||||
- Type names may need "Model" suffix to avoid clashes
|
||||
|
||||
3. **OpenIddict Multi-Server**:
|
||||
- Requires shared Data Protection key ring
|
||||
- All servers must have synchronized clocks (NTP)
|
||||
- Reference tokens require database storage
|
||||
|
||||
### External Dependencies
|
||||
|
||||
**OpenIddict**:
|
||||
- OAuth 2.0 / OpenID Connect provider
|
||||
- Version: See `Directory.Packages.props`
|
||||
- Uses ASP.NET Core Data Protection for token encryption
|
||||
|
||||
**Swashbuckle**:
|
||||
- OpenAPI 3.0 document generation
|
||||
- Custom filters: `EnumSchemaFilter`, `MimeTypeDocumentFilter`, `RemoveSecuritySchemesDocumentFilter`
|
||||
|
||||
**Asp.Versioning**:
|
||||
- API versioning via `ApiVersion` attribute
|
||||
- API explorer integration for multi-version Swagger docs
|
||||
|
||||
### Configuration
|
||||
|
||||
**HTTPS**: `DisableTransportSecurityRequirement` for local dev only (ConfigureOpenIddict.cs:14). **Warning**: Never disable in production.
|
||||
|
||||
### Usage Pattern
|
||||
|
||||
Consuming APIs call `builder.AddUmbracoApiOpenApiUI().AddUmbracoOpenIddict()`
|
||||
|
||||
---
|
||||
|
||||
## Quick Reference
|
||||
|
||||
### Essential Commands
|
||||
|
||||
```bash
|
||||
# Build project
|
||||
dotnet build src/Umbraco.Cms.Api.Common/Umbraco.Cms.Api.Common.csproj
|
||||
|
||||
# Pack for NuGet
|
||||
dotnet pack src/Umbraco.Cms.Api.Common/Umbraco.Cms.Api.Common.csproj -c Release
|
||||
|
||||
# Test via integration tests
|
||||
dotnet test tests/Umbraco.Tests.Integration/
|
||||
|
||||
# Check packages
|
||||
dotnet list src/Umbraco.Cms.Api.Common/Umbraco.Cms.Api.Common.csproj package --outdated
|
||||
dotnet list src/Umbraco.Cms.Api.Common/Umbraco.Cms.Api.Common.csproj package --vulnerable
|
||||
```
|
||||
|
||||
### Key Classes
|
||||
|
||||
| Class | Purpose | File |
|
||||
|-------|---------|------|
|
||||
| `ProblemDetailsBuilder` | Build RFC 7807 error responses | Builders/ProblemDetailsBuilder.cs |
|
||||
| `SchemaIdHandler` | Generate OpenAPI schema IDs | OpenApi/SchemaIdHandler.cs |
|
||||
| `UmbracoJsonTypeInfoResolver` | Polymorphic JSON serialization | Serialization/UmbracoJsonTypeInfoResolver.cs |
|
||||
| `UmbracoBuilderAuthExtensions` | Configure OpenIddict | DependencyInjection/UmbracoBuilderAuthExtensions.cs |
|
||||
| `HideBackOfficeTokensHandler` | Secure cookie-based token storage | DependencyInjection/HideBackOfficeTokensHandler.cs |
|
||||
| `PagedViewModel<T>` | Generic pagination model | ViewModels/Pagination/PagedViewModel.cs |
|
||||
|
||||
### Important Files
|
||||
|
||||
- `Umbraco.Cms.Api.Common.csproj` - Project dependencies
|
||||
- `DependencyInjection/UmbracoBuilderApiExtensions.cs` - OpenAPI registration (line 12-31)
|
||||
- `DependencyInjection/UmbracoBuilderAuthExtensions.cs` - OpenIddict setup (line 20-183)
|
||||
- `Security/Paths.cs` - API endpoint path constants
|
||||
|
||||
### Getting Help
|
||||
|
||||
- **Root documentation**: `/CLAUDE.md` - Repository overview
|
||||
- **Core patterns**: `/src/Umbraco.Core/CLAUDE.md` - Core contracts and patterns
|
||||
- **Official docs**: https://docs.umbraco.com/
|
||||
- **OpenIddict docs**: https://documentation.openiddict.com/
|
||||
|
||||
---
|
||||
|
||||
**This library is the foundation for all Umbraco CMS REST APIs. Focus on OpenAPI customization, authentication configuration, and polymorphic serialization when working here.**
|
||||
@@ -4,7 +4,7 @@ using Microsoft.AspNetCore.Mvc.ApiExplorer;
|
||||
using Microsoft.AspNetCore.Mvc.Controllers;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Microsoft.OpenApi;
|
||||
using Microsoft.OpenApi.Models;
|
||||
using Swashbuckle.AspNetCore.SwaggerGen;
|
||||
using Umbraco.Cms.Api.Common.OpenApi;
|
||||
using Umbraco.Extensions;
|
||||
|
||||
@@ -21,13 +21,9 @@ internal sealed class HideBackOfficeTokensHandler
|
||||
INotificationHandler<UserLogoutSuccessNotification>
|
||||
{
|
||||
private const string RedactedTokenValue = "[redacted]";
|
||||
|
||||
// The __Host- prefix enforces secure cookies at browser level (requires Secure, Path=/, no Domain).
|
||||
// For local development over HTTP, we use a simpler prefix to avoid browser rejection.
|
||||
private const string SecureCookiePrefix = "__Host-";
|
||||
private const string AccessTokenCookieName = "umbAccessToken";
|
||||
private const string RefreshTokenCookieName = "umbRefreshToken";
|
||||
private const string PkceCodeCookieName = "umbPkceCode";
|
||||
private const string AccessTokenCookieKey = "__Host-umbAccessToken";
|
||||
private const string RefreshTokenCookieKey = "__Host-umbRefreshToken";
|
||||
private const string PkceCodeCookieKey = "__Host-umbPkceCode";
|
||||
|
||||
private readonly IHttpContextAccessor _httpContextAccessor;
|
||||
private readonly IDataProtectionProvider _dataProtectionProvider;
|
||||
@@ -63,13 +59,13 @@ internal sealed class HideBackOfficeTokensHandler
|
||||
|
||||
if (context.Response.AccessToken is not null)
|
||||
{
|
||||
SetCookie(httpContext, AccessTokenCookieName, context.Response.AccessToken);
|
||||
SetCookie(httpContext, AccessTokenCookieKey, context.Response.AccessToken);
|
||||
context.Response.AccessToken = RedactedTokenValue;
|
||||
}
|
||||
|
||||
if (context.Response.RefreshToken is not null)
|
||||
{
|
||||
SetCookie(httpContext, RefreshTokenCookieName, context.Response.RefreshToken);
|
||||
SetCookie(httpContext, RefreshTokenCookieKey, context.Response.RefreshToken);
|
||||
context.Response.RefreshToken = RedactedTokenValue;
|
||||
}
|
||||
|
||||
@@ -91,7 +87,7 @@ internal sealed class HideBackOfficeTokensHandler
|
||||
|
||||
if (context.Response.Code is not null)
|
||||
{
|
||||
SetCookie(GetHttpContext(), PkceCodeCookieName, context.Response.Code);
|
||||
SetCookie(GetHttpContext(), PkceCodeCookieKey, context.Response.Code);
|
||||
context.Response.Code = RedactedTokenValue;
|
||||
}
|
||||
|
||||
@@ -109,16 +105,14 @@ internal sealed class HideBackOfficeTokensHandler
|
||||
return ValueTask.CompletedTask;
|
||||
}
|
||||
|
||||
HttpContext httpContext = GetHttpContext();
|
||||
|
||||
// Handle when the PKCE code is being exchanged for an access token.
|
||||
if (context.Request.Code == RedactedTokenValue
|
||||
&& TryGetCookie(httpContext, PkceCodeCookieName, out var code))
|
||||
&& TryGetCookie(PkceCodeCookieKey, out var code))
|
||||
{
|
||||
context.Request.Code = code;
|
||||
|
||||
// We won't need the PKCE cookie after this, let's remove it.
|
||||
RemoveCookie(httpContext, PkceCodeCookieName);
|
||||
RemoveCookie(GetHttpContext(), PkceCodeCookieKey);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -129,7 +123,7 @@ internal sealed class HideBackOfficeTokensHandler
|
||||
|
||||
// Handle when a refresh token is being exchanged for a new access token.
|
||||
if (context.Request.RefreshToken == RedactedTokenValue
|
||||
&& TryGetCookie(httpContext, RefreshTokenCookieName, out var refreshToken))
|
||||
&& TryGetCookie(RefreshTokenCookieKey, out var refreshToken))
|
||||
{
|
||||
context.Request.RefreshToken = refreshToken;
|
||||
}
|
||||
@@ -155,7 +149,7 @@ internal sealed class HideBackOfficeTokensHandler
|
||||
return ValueTask.CompletedTask;
|
||||
}
|
||||
|
||||
if (TryGetCookie(GetHttpContext(), AccessTokenCookieName, out var accessToken))
|
||||
if (TryGetCookie(AccessTokenCookieKey, out var accessToken))
|
||||
{
|
||||
context.AccessToken = accessToken;
|
||||
}
|
||||
@@ -165,8 +159,8 @@ internal sealed class HideBackOfficeTokensHandler
|
||||
|
||||
public void Handle(UserLogoutSuccessNotification notification)
|
||||
{
|
||||
HttpContext? httpContext = _httpContextAccessor.HttpContext;
|
||||
if (httpContext is null)
|
||||
HttpContext? context = _httpContextAccessor.HttpContext;
|
||||
if (context is null)
|
||||
{
|
||||
// For some reason there is no ambient HTTP context, so we can't clean up the cookies.
|
||||
// This is OK, because the tokens in the cookies have already been revoked at user sign-out,
|
||||
@@ -174,32 +168,23 @@ internal sealed class HideBackOfficeTokensHandler
|
||||
return;
|
||||
}
|
||||
|
||||
RemoveCookie(httpContext, AccessTokenCookieName);
|
||||
RemoveCookie(httpContext, RefreshTokenCookieName);
|
||||
context.Response.Cookies.Delete(AccessTokenCookieKey);
|
||||
context.Response.Cookies.Delete(RefreshTokenCookieKey);
|
||||
}
|
||||
|
||||
private HttpContext GetHttpContext()
|
||||
=> _httpContextAccessor.GetRequiredHttpContext();
|
||||
|
||||
private string GetCookieKey(HttpContext httpContext, string cookieName)
|
||||
=> _globalSettings.UseHttps || httpContext.Request.IsHttps
|
||||
? $"{SecureCookiePrefix}{cookieName}"
|
||||
: cookieName;
|
||||
|
||||
private void SetCookie(HttpContext httpContext, string cookieName, string value)
|
||||
private void SetCookie(HttpContext httpContext, string key, string value)
|
||||
{
|
||||
var key = GetCookieKey(httpContext, cookieName);
|
||||
var cookieValue = EncryptionHelper.Encrypt(value, _dataProtectionProvider);
|
||||
|
||||
RemoveCookie(httpContext, cookieName);
|
||||
RemoveCookie(httpContext, key);
|
||||
httpContext.Response.Cookies.Append(key, cookieValue, GetCookieOptions(httpContext));
|
||||
}
|
||||
|
||||
private void RemoveCookie(HttpContext httpContext, string cookieName)
|
||||
{
|
||||
var key = GetCookieKey(httpContext, cookieName);
|
||||
httpContext.Response.Cookies.Delete(key, GetCookieOptions(httpContext));
|
||||
}
|
||||
private void RemoveCookie(HttpContext httpContext, string key)
|
||||
=> httpContext.Response.Cookies.Delete(key, GetCookieOptions(httpContext));
|
||||
|
||||
private CookieOptions GetCookieOptions(HttpContext httpContext) =>
|
||||
new()
|
||||
@@ -226,10 +211,9 @@ internal sealed class HideBackOfficeTokensHandler
|
||||
SameSite = ParseSameSiteMode(_backOfficeTokenCookieSettings.SameSite),
|
||||
};
|
||||
|
||||
private bool TryGetCookie(HttpContext httpContext, string cookieName, [NotNullWhen(true)] out string? value)
|
||||
private bool TryGetCookie(string key, [NotNullWhen(true)] out string? value)
|
||||
{
|
||||
var key = GetCookieKey(httpContext, cookieName);
|
||||
if (httpContext.Request.Cookies.TryGetValue(key, out var cookieValue))
|
||||
if (GetHttpContext().Request.Cookies.TryGetValue(key, out var cookieValue))
|
||||
{
|
||||
value = EncryptionHelper.Decrypt(cookieValue, _dataProtectionProvider);
|
||||
return true;
|
||||
|
||||
@@ -9,9 +9,9 @@ using Umbraco.Cms.Api.Common.Security;
|
||||
using Umbraco.Cms.Core;
|
||||
using Umbraco.Cms.Core.Configuration.Models;
|
||||
using Umbraco.Cms.Core.DependencyInjection;
|
||||
using Umbraco.Cms.Infrastructure.BackgroundJobs;
|
||||
using Umbraco.Cms.Infrastructure.BackgroundJobs.Jobs.DistributedJobs;
|
||||
using Umbraco.Cms.Core.Notifications;
|
||||
using Umbraco.Cms.Infrastructure.BackgroundJobs.Jobs;
|
||||
using Umbraco.Extensions;
|
||||
|
||||
namespace Umbraco.Cms.Api.Common.DependencyInjection;
|
||||
|
||||
@@ -29,6 +29,11 @@ public static class UmbracoBuilderAuthExtensions
|
||||
|
||||
private static void ConfigureOpenIddict(IUmbracoBuilder builder)
|
||||
{
|
||||
// Optionally hide tokens from the back-office.
|
||||
var hideBackOfficeTokens = (builder.Config
|
||||
.GetSection(Constants.Configuration.ConfigBackOfficeTokenCookie)
|
||||
.Get<BackOfficeTokenCookieSettings>() ?? new BackOfficeTokenCookieSettings()).Enabled;
|
||||
|
||||
builder.Services.AddOpenIddict()
|
||||
// Register the OpenIddict server components.
|
||||
.AddServer(options =>
|
||||
@@ -115,24 +120,27 @@ public static class UmbracoBuilderAuthExtensions
|
||||
configuration.UseSingletonHandler<ProcessRequestContextHandler>().SetOrder(OpenIddict.Server.AspNetCore.OpenIddictServerAspNetCoreHandlers.ResolveRequestUri.Descriptor.Order - 1);
|
||||
});
|
||||
|
||||
options.AddEventHandler<OpenIddictServerEvents.ApplyTokenResponseContext>(configuration =>
|
||||
if (hideBackOfficeTokens)
|
||||
{
|
||||
configuration
|
||||
.UseSingletonHandler<HideBackOfficeTokensHandler>()
|
||||
.SetOrder(OpenIddict.Server.AspNetCore.OpenIddictServerAspNetCoreHandlers.ProcessJsonResponse<OpenIddictServerEvents.ApplyTokenResponseContext>.Descriptor.Order - 1);
|
||||
});
|
||||
options.AddEventHandler<OpenIddictServerEvents.ApplyAuthorizationResponseContext>(configuration =>
|
||||
{
|
||||
configuration
|
||||
.UseSingletonHandler<HideBackOfficeTokensHandler>()
|
||||
.SetOrder(OpenIddict.Server.AspNetCore.OpenIddictServerAspNetCoreHandlers.Authentication.ProcessQueryResponse.Descriptor.Order - 1);
|
||||
});
|
||||
options.AddEventHandler<OpenIddictServerEvents.ExtractTokenRequestContext>(configuration =>
|
||||
{
|
||||
configuration
|
||||
.UseSingletonHandler<HideBackOfficeTokensHandler>()
|
||||
.SetOrder(OpenIddict.Server.AspNetCore.OpenIddictServerAspNetCoreHandlers.ExtractPostRequest<OpenIddictServerEvents.ExtractTokenRequestContext>.Descriptor.Order + 1);
|
||||
});
|
||||
options.AddEventHandler<OpenIddictServerEvents.ApplyTokenResponseContext>(configuration =>
|
||||
{
|
||||
configuration
|
||||
.UseSingletonHandler<HideBackOfficeTokensHandler>()
|
||||
.SetOrder(OpenIddict.Server.AspNetCore.OpenIddictServerAspNetCoreHandlers.ProcessJsonResponse<OpenIddictServerEvents.ApplyTokenResponseContext>.Descriptor.Order - 1);
|
||||
});
|
||||
options.AddEventHandler<OpenIddictServerEvents.ApplyAuthorizationResponseContext>(configuration =>
|
||||
{
|
||||
configuration
|
||||
.UseSingletonHandler<HideBackOfficeTokensHandler>()
|
||||
.SetOrder(OpenIddict.Server.AspNetCore.OpenIddictServerAspNetCoreHandlers.Authentication.ProcessQueryResponse.Descriptor.Order - 1);
|
||||
});
|
||||
options.AddEventHandler<OpenIddictServerEvents.ExtractTokenRequestContext>(configuration =>
|
||||
{
|
||||
configuration
|
||||
.UseSingletonHandler<HideBackOfficeTokensHandler>()
|
||||
.SetOrder(OpenIddict.Server.AspNetCore.OpenIddictServerAspNetCoreHandlers.ExtractPostRequest<OpenIddictServerEvents.ExtractTokenRequestContext>.Descriptor.Order + 1);
|
||||
});
|
||||
}
|
||||
})
|
||||
|
||||
// Register the OpenIddict validation components.
|
||||
@@ -158,18 +166,24 @@ public static class UmbracoBuilderAuthExtensions
|
||||
configuration.UseSingletonHandler<ProcessRequestContextHandler>().SetOrder(OpenIddict.Validation.AspNetCore.OpenIddictValidationAspNetCoreHandlers.ResolveRequestUri.Descriptor.Order - 1);
|
||||
});
|
||||
|
||||
options.AddEventHandler<OpenIddictValidationEvents.ProcessAuthenticationContext>(configuration =>
|
||||
if (hideBackOfficeTokens)
|
||||
{
|
||||
configuration
|
||||
.UseSingletonHandler<HideBackOfficeTokensHandler>()
|
||||
// IMPORTANT: the handler must be AFTER the built-in query string handler, because the client-side SignalR library sometimes appends access tokens to the query string.
|
||||
.SetOrder(OpenIddict.Validation.AspNetCore.OpenIddictValidationAspNetCoreHandlers.ExtractAccessTokenFromQueryString.Descriptor.Order + 1);
|
||||
});
|
||||
options.AddEventHandler<OpenIddictValidationEvents.ProcessAuthenticationContext>(configuration =>
|
||||
{
|
||||
configuration
|
||||
.UseSingletonHandler<HideBackOfficeTokensHandler>()
|
||||
// IMPORTANT: the handler must be AFTER the built-in query string handler, because the client-side SignalR library sometimes appends access tokens to the query string.
|
||||
.SetOrder(OpenIddict.Validation.AspNetCore.OpenIddictValidationAspNetCoreHandlers.ExtractAccessTokenFromQueryString.Descriptor.Order + 1);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
builder.Services.AddSingleton<IDistributedBackgroundJob, OpenIddictCleanupJob>();
|
||||
builder.Services.AddRecurringBackgroundJob<OpenIddictCleanupJob>();
|
||||
builder.Services.ConfigureOptions<ConfigureOpenIddict>();
|
||||
|
||||
builder.AddNotificationHandler<UserLogoutSuccessNotification, HideBackOfficeTokensHandler>();
|
||||
if (hideBackOfficeTokens)
|
||||
{
|
||||
builder.AddNotificationHandler<UserLogoutSuccessNotification, HideBackOfficeTokensHandler>();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,11 +8,11 @@ namespace Umbraco.Extensions;
|
||||
public static class MethodInfoApiCommonExtensions
|
||||
{
|
||||
|
||||
public static string GetMapToApiVersionAttributeValue(this MethodInfo methodInfo)
|
||||
public static string? GetMapToApiVersionAttributeValue(this MethodInfo methodInfo)
|
||||
{
|
||||
MapToApiVersionAttribute[] mapToApis = methodInfo.GetCustomAttributes(typeof(MapToApiVersionAttribute), inherit: true).Cast<MapToApiVersionAttribute>().ToArray();
|
||||
|
||||
return string.Join("|", mapToApis.SelectMany(x => x.Versions));
|
||||
return string.Join("|", mapToApis.SelectMany(x=>x.Versions));
|
||||
}
|
||||
|
||||
public static string? GetMapToApiAttributeValue(this MethodInfo methodInfo)
|
||||
|
||||
@@ -1,27 +1,25 @@
|
||||
using System.Reflection;
|
||||
using System.Runtime.Serialization;
|
||||
using System.Text.Json.Nodes;
|
||||
using Microsoft.OpenApi;
|
||||
using Microsoft.OpenApi.Any;
|
||||
using Microsoft.OpenApi.Models;
|
||||
using Swashbuckle.AspNetCore.SwaggerGen;
|
||||
|
||||
namespace Umbraco.Cms.Api.Common.OpenApi;
|
||||
|
||||
public class EnumSchemaFilter : ISchemaFilter
|
||||
{
|
||||
public void Apply(IOpenApiSchema model, SchemaFilterContext context)
|
||||
public void Apply(OpenApiSchema model, SchemaFilterContext context)
|
||||
{
|
||||
if (model is not OpenApiSchema schema || context.Type.IsEnum is false)
|
||||
if (context.Type.IsEnum)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
schema.Type = JsonSchemaType.String;
|
||||
schema.Format = null;
|
||||
schema.Enum = new List<JsonNode>();
|
||||
foreach (var name in Enum.GetNames(context.Type))
|
||||
{
|
||||
var actualName = context.Type.GetField(name)?.GetCustomAttribute<EnumMemberAttribute>()?.Value ?? name;
|
||||
schema.Enum.Add(actualName);
|
||||
model.Type = "string";
|
||||
model.Format = null;
|
||||
model.Enum.Clear();
|
||||
foreach (var name in Enum.GetNames(context.Type))
|
||||
{
|
||||
var actualName = context.Type.GetField(name)?.GetCustomAttribute<EnumMemberAttribute>()?.Value ?? name;
|
||||
model.Enum.Add(new OpenApiString(actualName));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using Microsoft.OpenApi;
|
||||
using Microsoft.OpenApi.Models;
|
||||
using Swashbuckle.AspNetCore.SwaggerGen;
|
||||
using Umbraco.Extensions;
|
||||
|
||||
@@ -21,32 +21,25 @@ public class MimeTypeDocumentFilter : IDocumentFilter
|
||||
}
|
||||
|
||||
OpenApiOperation[] operations = swaggerDoc.Paths
|
||||
.SelectMany(path => path.Value.Operations?.Values ?? Enumerable.Empty<OpenApiOperation>())
|
||||
.SelectMany(path => path.Value.Operations.Values)
|
||||
.ToArray();
|
||||
|
||||
void RemoveUnwantedMimeTypes(IDictionary<string, OpenApiMediaType>? content)
|
||||
void RemoveUnwantedMimeTypes(IDictionary<string, OpenApiMediaType> content)
|
||||
{
|
||||
if (content is null || content.ContainsKey("application/json") is false)
|
||||
if (content.ContainsKey("application/json"))
|
||||
{
|
||||
return;
|
||||
content.RemoveAll(r => r.Key != "application/json");
|
||||
}
|
||||
|
||||
content.RemoveAll(r => r.Key != "application/json");
|
||||
}
|
||||
|
||||
OpenApiRequestBody[] requestBodies = operations
|
||||
.Select(operation => operation.RequestBody)
|
||||
.OfType<OpenApiRequestBody>()
|
||||
.ToArray();
|
||||
OpenApiRequestBody[] requestBodies = operations.Select(operation => operation.RequestBody).WhereNotNull().ToArray();
|
||||
foreach (OpenApiRequestBody requestBody in requestBodies)
|
||||
{
|
||||
RemoveUnwantedMimeTypes(requestBody.Content);
|
||||
}
|
||||
|
||||
OpenApiResponse[] responses = operations
|
||||
.SelectMany(operation => operation.Responses?.Values ?? Enumerable.Empty<IOpenApiResponse>())
|
||||
.OfType<OpenApiResponse>()
|
||||
.ToArray();
|
||||
OpenApiResponse[] responses = operations.SelectMany(operation => operation.Responses.Values).WhereNotNull().ToArray();
|
||||
foreach (OpenApiResponse response in responses)
|
||||
{
|
||||
RemoveUnwantedMimeTypes(response.Content);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using Microsoft.OpenApi;
|
||||
using Microsoft.OpenApi.Models;
|
||||
using Swashbuckle.AspNetCore.SwaggerGen;
|
||||
|
||||
namespace Umbraco.Cms.Api.Common.OpenApi;
|
||||
@@ -20,6 +20,6 @@ public class RemoveSecuritySchemesDocumentFilter : IDocumentFilter
|
||||
return;
|
||||
}
|
||||
|
||||
swaggerDoc.Components?.SecuritySchemes?.Clear();
|
||||
swaggerDoc.Components.SecuritySchemes.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,20 @@ public class SubTypesSelector : ISubTypesSelector
|
||||
private readonly IEnumerable<ISubTypesHandler> _subTypeHandlers;
|
||||
private readonly IUmbracoJsonTypeInfoResolver _umbracoJsonTypeInfoResolver;
|
||||
|
||||
[Obsolete("The settings parameter is not required anymore, use the other constructor instead. Scheduled for removal in Umbraco 17.")]
|
||||
public SubTypesSelector(
|
||||
IOptions<GlobalSettings> settings,
|
||||
IHostingEnvironment hostingEnvironment,
|
||||
IHttpContextAccessor httpContextAccessor,
|
||||
IEnumerable<ISubTypesHandler> subTypeHandlers,
|
||||
IUmbracoJsonTypeInfoResolver umbracoJsonTypeInfoResolver)
|
||||
{
|
||||
_hostingEnvironment = hostingEnvironment;
|
||||
_httpContextAccessor = httpContextAccessor;
|
||||
_subTypeHandlers = subTypeHandlers;
|
||||
_umbracoJsonTypeInfoResolver = umbracoJsonTypeInfoResolver;
|
||||
}
|
||||
|
||||
public SubTypesSelector(
|
||||
IHostingEnvironment hostingEnvironment,
|
||||
IHttpContextAccessor httpContextAccessor,
|
||||
|
||||
@@ -3,12 +3,13 @@ using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Microsoft.OpenApi;
|
||||
using Microsoft.OpenApi.Models;
|
||||
using Swashbuckle.AspNetCore.SwaggerGen;
|
||||
using Swashbuckle.AspNetCore.SwaggerUI;
|
||||
using Umbraco.Cms.Core;
|
||||
using Umbraco.Cms.Core.Hosting;
|
||||
using Umbraco.Cms.Web.Common.ApplicationBuilder;
|
||||
using Umbraco.Extensions;
|
||||
using IHostingEnvironment = Umbraco.Cms.Core.Hosting.IHostingEnvironment;
|
||||
|
||||
namespace Umbraco.Cms.Api.Common.OpenApi;
|
||||
|
||||
-84
@@ -1,84 +0,0 @@
|
||||
using System.Security.Claims;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.Options;
|
||||
using OpenIddict.Server;
|
||||
using Umbraco.Cms.Core.Configuration.Models;
|
||||
using Umbraco.Cms.Core.Security;
|
||||
using Umbraco.Extensions;
|
||||
|
||||
namespace Umbraco.Cms.Infrastructure.Security;
|
||||
|
||||
/// <summary>
|
||||
/// Provides OpenIddict server event handlers to expose the backoffice authentication token via a custom authentication scheme.
|
||||
/// </summary>
|
||||
public class ExposeBackOfficeAuthenticationOpenIddictServerEventsHandler : IOpenIddictServerHandler<OpenIddictServerEvents.GenerateTokenContext>,
|
||||
IOpenIddictServerHandler<OpenIddictServerEvents.ApplyRevocationResponseContext>
|
||||
{
|
||||
private readonly IHttpContextAccessor _httpContextAccessor;
|
||||
private readonly string[] _claimTypes;
|
||||
private readonly TimeSpan _timeOut;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ExposeBackOfficeAuthenticationOpenIddictServerEventsHandler"/> class.
|
||||
/// </summary>
|
||||
public ExposeBackOfficeAuthenticationOpenIddictServerEventsHandler(
|
||||
IHttpContextAccessor httpContextAccessor,
|
||||
IOptions<GlobalSettings> globalSettings,
|
||||
IOptions<BackOfficeIdentityOptions> backOfficeIdentityOptions)
|
||||
{
|
||||
_httpContextAccessor = httpContextAccessor;
|
||||
_timeOut = globalSettings.Value.TimeOut;
|
||||
|
||||
// These are the type identifiers for the claims required by the principal
|
||||
// for the custom authentication scheme.
|
||||
// We make available the ID, user name and allowed applications (sections) claims.
|
||||
_claimTypes =
|
||||
[
|
||||
backOfficeIdentityOptions.Value.ClaimsIdentity.UserIdClaimType,
|
||||
backOfficeIdentityOptions.Value.ClaimsIdentity.UserNameClaimType,
|
||||
Core.Constants.Security.AllowedApplicationsClaimType,
|
||||
];
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
/// <remarks>
|
||||
/// Event handler for when access tokens are generated (created or refreshed).
|
||||
/// </remarks>
|
||||
public async ValueTask HandleAsync(OpenIddictServerEvents.GenerateTokenContext context)
|
||||
{
|
||||
// Only proceed if this is a back-office sign-in.
|
||||
if (context.Principal.Identity?.AuthenticationType != Core.Constants.Security.BackOfficeAuthenticationType)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Create a new principal with the claims from the authenticated principal.
|
||||
var principal = new ClaimsPrincipal(
|
||||
new ClaimsIdentity(
|
||||
context.Principal.Claims.Where(claim => _claimTypes.Contains(claim.Type)),
|
||||
Core.Constants.Security.BackOfficeExposedAuthenticationType));
|
||||
|
||||
// Sign-in the new principal for the custom authentication scheme.
|
||||
await _httpContextAccessor
|
||||
.GetRequiredHttpContext()
|
||||
.SignInAsync(Core.Constants.Security.BackOfficeExposedAuthenticationType, principal, GetAuthenticationProperties());
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
/// <remarks>
|
||||
/// Event handler for when access tokens are revoked.
|
||||
/// </remarks>
|
||||
public async ValueTask HandleAsync(OpenIddictServerEvents.ApplyRevocationResponseContext context)
|
||||
=> await _httpContextAccessor
|
||||
.GetRequiredHttpContext()
|
||||
.SignOutAsync(Core.Constants.Security.BackOfficeExposedAuthenticationType, GetAuthenticationProperties());
|
||||
|
||||
private AuthenticationProperties GetAuthenticationProperties()
|
||||
=> new()
|
||||
{
|
||||
IsPersistent = true,
|
||||
IssuedUtc = DateTimeOffset.UtcNow,
|
||||
ExpiresUtc = DateTimeOffset.UtcNow.Add(_timeOut)
|
||||
};
|
||||
}
|
||||
@@ -16,6 +16,11 @@
|
||||
<PackageReference Include="OpenIddict.AspNetCore" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<!-- Take top-level depedendency on OpenIddict.AspNetCore depends on a vulnerable version -->
|
||||
<PackageReference Include="Microsoft.Extensions.Caching.Memory" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Umbraco.Core\Umbraco.Core.csproj" />
|
||||
<ProjectReference Include="..\Umbraco.Web.Common\Umbraco.Web.Common.csproj" />
|
||||
|
||||
@@ -1,382 +0,0 @@
|
||||
# Umbraco.Cms.Api.Delivery
|
||||
|
||||
Headless content delivery REST API for Umbraco CMS. Enables frontend applications to fetch published content, media, and member-protected resources.
|
||||
|
||||
---
|
||||
|
||||
## 1. Architecture
|
||||
|
||||
**Type**: Class Library (NuGet Package)
|
||||
**Target Framework**: .NET 10.0
|
||||
**Purpose**: Content Delivery API for headless CMS scenarios
|
||||
|
||||
### Key Technologies
|
||||
|
||||
- **ASP.NET Core** - Web framework
|
||||
- **OpenIddict** - Member authentication (OAuth 2.0)
|
||||
- **Asp.Versioning** - API versioning (V1, V2)
|
||||
- **Output Caching** - Configurable response caching
|
||||
- **Examine/Lucene** - Content querying
|
||||
|
||||
### Dependencies
|
||||
|
||||
- `Umbraco.Cms.Api.Common` - Shared API infrastructure (OpenAPI, auth)
|
||||
- `Umbraco.Web.Common` - Web functionality
|
||||
|
||||
### Project Structure (86 files)
|
||||
|
||||
```
|
||||
Umbraco.Cms.Api.Delivery/
|
||||
├── Controllers/
|
||||
│ ├── Content/ # Content endpoints (by ID, route, query)
|
||||
│ ├── Media/ # Media endpoints (by ID, path, query)
|
||||
│ └── Security/ # Member auth (authorize, token, signout)
|
||||
├── Querying/
|
||||
│ ├── Filters/ # ContentType, Name, CreateDate, UpdateDate
|
||||
│ ├── Selectors/ # Ancestors, Children, Descendants
|
||||
│ └── Sorts/ # Name, CreateDate, UpdateDate, Level, SortOrder
|
||||
├── Indexing/ # Lucene index field handlers
|
||||
├── Services/ # Business logic and query building
|
||||
├── Caching/ # Output cache policies
|
||||
├── Rendering/ # Output expansion strategies
|
||||
├── Configuration/ # Swagger configuration
|
||||
└── Filters/ # Action filters (access, validation)
|
||||
```
|
||||
|
||||
### Design Patterns
|
||||
|
||||
1. **Strategy Pattern** - Query handlers (`ISelectorHandler`, `IFilterHandler`, `ISortHandler`)
|
||||
2. **Factory Pattern** - `ApiContentQueryFactory` builds Examine queries
|
||||
3. **Template Method** - `ContentApiControllerBase` for shared controller logic
|
||||
4. **Options Pattern** - `DeliveryApiSettings` for all configuration
|
||||
|
||||
---
|
||||
|
||||
## 2. Commands
|
||||
|
||||
See "Quick Reference" section at bottom for common commands.
|
||||
|
||||
---
|
||||
|
||||
## 3. Key Patterns
|
||||
|
||||
### API Versioning (V1 vs V2)
|
||||
|
||||
**V1** (legacy) and **V2** (current) coexist. Key difference is output expansion:
|
||||
|
||||
```csharp
|
||||
// DependencyInjection/UmbracoBuilderExtensions.cs:49-52
|
||||
// V1 uses RequestContextOutputExpansionStrategy
|
||||
// V2+ uses RequestContextOutputExpansionStrategyV2
|
||||
return apiVersion.MajorVersion == 1
|
||||
? provider.GetRequiredService<RequestContextOutputExpansionStrategy>()
|
||||
: provider.GetRequiredService<RequestContextOutputExpansionStrategyV2>();
|
||||
```
|
||||
|
||||
**Why V2**: Improved `expand` and `fields` query parameter parsing (tree-based).
|
||||
|
||||
### Query System Architecture
|
||||
|
||||
Content querying flows through handlers registered in DI:
|
||||
|
||||
1. **Selectors** (`fetch` parameter): `ancestors:id`, `children:id`, `descendants:id`
|
||||
2. **Filters** (`filter[]` parameter): `contentType:alias`, `name:value`, `createDate>2024-01-01`
|
||||
3. **Sorts** (`sort[]` parameter): `name:asc`, `createDate:desc`, `level:asc`
|
||||
|
||||
```csharp
|
||||
// Services/ApiContentQueryService.cs:91-96
|
||||
ISelectorHandler? selectorHandler = _selectorHandlers.FirstOrDefault(h => h.CanHandle(fetch));
|
||||
return selectorHandler?.BuildSelectorOption(fetch);
|
||||
```
|
||||
|
||||
### Path Decoding Workaround
|
||||
|
||||
ASP.NET Core doesn't decode forward slashes in route parameters:
|
||||
|
||||
```csharp
|
||||
// Controllers/DeliveryApiControllerBase.cs:21-31
|
||||
// OpenAPI clients URL-encode paths, but ASP.NET Core doesn't decode "/"
|
||||
// See https://github.com/dotnet/aspnetcore/issues/11544
|
||||
if (path.Contains("%2F", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
path = WebUtility.UrlDecode(path);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Testing
|
||||
|
||||
**Location**: No direct tests - tested via integration tests in test projects
|
||||
|
||||
```bash
|
||||
dotnet test tests/Umbraco.Tests.Integration/ --filter "FullyQualifiedName~Delivery"
|
||||
```
|
||||
|
||||
**Internals exposed to** (csproj lines 28-36):
|
||||
- `Umbraco.Tests.UnitTests`
|
||||
- `Umbraco.Tests.Integration`
|
||||
- `DynamicProxyGenAssembly2` (for mocking)
|
||||
|
||||
**Focus areas**:
|
||||
- Query parsing (selectors, filters, sorts)
|
||||
- Member authentication flows
|
||||
- Output caching behavior
|
||||
- Protected content access
|
||||
|
||||
---
|
||||
|
||||
## 5. Security & Access Control
|
||||
|
||||
### Three Access Modes
|
||||
|
||||
```csharp
|
||||
// Services/ApiAccessService.cs:21-27
|
||||
public bool HasPublicAccess() => _deliveryApiSettings.PublicAccess || HasValidApiKey();
|
||||
public bool HasPreviewAccess() => HasValidApiKey();
|
||||
public bool HasMediaAccess() => _deliveryApiSettings is { PublicAccess: true, Media.PublicAccess: true } || HasValidApiKey();
|
||||
```
|
||||
|
||||
**Access levels**:
|
||||
1. **Public** - No authentication required (if enabled)
|
||||
2. **API Key** - Via `Api-Key` header
|
||||
3. **Preview** - Always requires API key
|
||||
|
||||
### Member Authentication
|
||||
|
||||
OpenIddict-based OAuth 2.0 for member-protected content:
|
||||
|
||||
**Flows supported** (Controllers/Security/MemberController.cs):
|
||||
- Authorization Code + PKCE (line 53)
|
||||
- Client Credentials (line 112)
|
||||
- Refresh Token (line 98)
|
||||
|
||||
**Endpoints**:
|
||||
- `GET /umbraco/delivery/api/v1/security/member/authorize`
|
||||
- `POST /umbraco/delivery/api/v1/security/member/token`
|
||||
- `GET /umbraco/delivery/api/v1/security/member/signout`
|
||||
|
||||
**Scopes**: Only `openid` and `offline_access` allowed for members (line 220-222)
|
||||
|
||||
### Protected Content
|
||||
|
||||
Member access checked via `ProtectedAccess` model:
|
||||
|
||||
```csharp
|
||||
// Controllers/Content/QueryContentApiController.cs:56-57
|
||||
ProtectedAccess protectedAccess = await _requestMemberAccessService.MemberAccessAsync();
|
||||
Attempt<PagedModel<Guid>, ApiContentQueryOperationStatus> queryAttempt =
|
||||
_apiContentQueryService.ExecuteQuery(fetch, filter, sort, protectedAccess, skip, take);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. Output Caching
|
||||
|
||||
### Cache Policy Configuration
|
||||
|
||||
```csharp
|
||||
// DependencyInjection/UmbracoBuilderExtensions.cs:120-136
|
||||
// Content and Media have separate cache durations
|
||||
options.AddPolicy(
|
||||
Constants.DeliveryApi.OutputCache.ContentCachePolicy,
|
||||
new DeliveryApiOutputCachePolicy(
|
||||
outputCacheSettings.ContentDuration,
|
||||
new StringValues([AcceptLanguage, AcceptSegment, StartItem])));
|
||||
```
|
||||
|
||||
**Cache invalidation conditions** (Caching/DeliveryApiOutputCachePolicy.cs:31):
|
||||
```csharp
|
||||
// Never cache preview or non-public access
|
||||
context.EnableOutputCaching = requestPreviewService.IsPreview() is false
|
||||
&& apiAccessService.HasPublicAccess();
|
||||
```
|
||||
|
||||
**Vary by headers**: `Accept-Language`, `Accept-Segment`, `Start-Item`
|
||||
|
||||
---
|
||||
|
||||
## 7. Edge Cases & Known Issues
|
||||
|
||||
### Technical Debt (TODOs in codebase)
|
||||
|
||||
1. **V1 Removal Pending** (4 locations):
|
||||
- `DependencyInjection/UmbracoBuilderExtensions.cs:98` - FIXME: remove matcher policy
|
||||
- `Routing/DeliveryApiItemsEndpointsMatcherPolicy.cs:11` - FIXME: remove class
|
||||
- `Filters/SwaggerDocumentationFilterBase.cs:79,83` - FIXME: remove V1 swagger docs
|
||||
|
||||
2. **Obsolete Reference Warnings** (csproj:9-13):
|
||||
- `ASP0019` - IHeaderDictionary.Append usage
|
||||
- `CS0618/CS0612` - Obsolete member references
|
||||
|
||||
### Empty Query Results
|
||||
|
||||
Query service returns empty results (not errors) for invalid options:
|
||||
|
||||
```csharp
|
||||
// Services/ApiContentQueryService.cs:54-78
|
||||
// Invalid selector/filter/sort returns fail status with empty result
|
||||
return Attempt.FailWithStatus(ApiContentQueryOperationStatus.SelectorOptionNotFound, emptyResult);
|
||||
```
|
||||
|
||||
### Start Item Fallback
|
||||
|
||||
When no `fetch` parameter provided, uses start item or all content:
|
||||
|
||||
```csharp
|
||||
// Services/ApiContentQueryService.cs:99-112
|
||||
if (_requestStartItemProviderAccessor.TryGetValue(out IRequestStartItemProvider? requestStartItemProvider))
|
||||
{
|
||||
IPublishedContent? startItem = requestStartItemProvider.GetStartItem();
|
||||
// Use descendants of start item
|
||||
}
|
||||
return _apiContentQueryProvider.AllContentSelectorOption(); // Fallback to all
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. Project-Specific Notes
|
||||
|
||||
### V1 vs V2 Differences
|
||||
|
||||
| Feature | V1 | V2 |
|
||||
|---------|----|----|
|
||||
| Output expansion | Basic | Tree-based parsing |
|
||||
| `expand` parameter | Flat list | Nested syntax |
|
||||
| `fields` parameter | Limited | Full property selection |
|
||||
| Default expansion strategy | `RequestContextOutputExpansionStrategy` | `RequestContextOutputExpansionStrategyV2` |
|
||||
|
||||
**Migration note**: V1 is deprecated; plan removal when V17+ drops V1 support.
|
||||
|
||||
### JSON Configuration
|
||||
|
||||
Delivery API has its own JSON options (distinct from Management API):
|
||||
|
||||
```csharp
|
||||
// DependencyInjection/UmbracoBuilderExtensions.cs:82-88
|
||||
.AddJsonOptions(Constants.JsonOptionsNames.DeliveryApi, options =>
|
||||
{
|
||||
options.JsonSerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.CamelCase;
|
||||
options.JsonSerializerOptions.TypeInfoResolver = new DeliveryApiJsonTypeResolver();
|
||||
options.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter());
|
||||
});
|
||||
```
|
||||
|
||||
### Member Token Revocation
|
||||
|
||||
Tokens automatically revoked on member changes:
|
||||
|
||||
```csharp
|
||||
// DependencyInjection/UmbracoBuilderExtensions.cs:93-96
|
||||
builder.AddNotificationAsyncHandler<MemberSavedNotification, RevokeMemberAuthenticationTokensNotificationHandler>();
|
||||
builder.AddNotificationAsyncHandler<MemberDeletedNotification, RevokeMemberAuthenticationTokensNotificationHandler>();
|
||||
builder.AddNotificationAsyncHandler<AssignedMemberRolesNotification, RevokeMemberAuthenticationTokensNotificationHandler>();
|
||||
builder.AddNotificationAsyncHandler<RemovedMemberRolesNotification, RevokeMemberAuthenticationTokensNotificationHandler>();
|
||||
```
|
||||
|
||||
### External Dependencies
|
||||
|
||||
**Examine/Lucene** (via Core):
|
||||
- Powers content querying
|
||||
- Selector/Filter/Sort handlers build Lucene queries
|
||||
|
||||
**OpenIddict** (via Api.Common):
|
||||
- Member OAuth 2.0 authentication
|
||||
- Reference tokens (not JWT)
|
||||
|
||||
### Configuration (appsettings.json)
|
||||
|
||||
```json
|
||||
{
|
||||
"Umbraco": {
|
||||
"CMS": {
|
||||
"DeliveryApi": {
|
||||
"Enabled": true,
|
||||
"PublicAccess": true,
|
||||
"ApiKey": "your-api-key",
|
||||
"Media": {
|
||||
"Enabled": true,
|
||||
"PublicAccess": true
|
||||
},
|
||||
"MemberAuthorization": {
|
||||
"AuthorizationCodeFlow": { "Enabled": true },
|
||||
"ClientCredentialsFlow": { "Enabled": false }
|
||||
},
|
||||
"OutputCache": {
|
||||
"Enabled": true,
|
||||
"ContentDuration": "00:01:00",
|
||||
"MediaDuration": "00:01:00"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### API Endpoints Summary
|
||||
|
||||
**Content** (`/umbraco/delivery/api/v2/content`):
|
||||
- `GET /item/{id}` - Single content by GUID
|
||||
- `GET /item/{path}` - Single content by route
|
||||
- `GET /items` - Multiple by IDs
|
||||
- `GET /` - Query with fetch/filter/sort
|
||||
|
||||
**Media** (`/umbraco/delivery/api/v2/media`):
|
||||
- `GET /item/{id}` - Single media by GUID
|
||||
- `GET /item/{path}` - Single media by path
|
||||
- `GET /items` - Multiple by IDs
|
||||
- `GET /` - Query media
|
||||
|
||||
**Security** (`/umbraco/delivery/api/v1/security/member`):
|
||||
- `GET /authorize` - Start OAuth flow
|
||||
- `POST /token` - Exchange code for token
|
||||
- `GET /signout` - Revoke session
|
||||
|
||||
---
|
||||
|
||||
## Quick Reference
|
||||
|
||||
### Essential Commands
|
||||
|
||||
```bash
|
||||
# Build project
|
||||
dotnet build src/Umbraco.Cms.Api.Delivery/Umbraco.Cms.Api.Delivery.csproj
|
||||
|
||||
# Pack for NuGet
|
||||
dotnet pack src/Umbraco.Cms.Api.Delivery/Umbraco.Cms.Api.Delivery.csproj -c Release
|
||||
|
||||
# Run integration tests
|
||||
dotnet test tests/Umbraco.Tests.Integration/ --filter "FullyQualifiedName~Delivery"
|
||||
|
||||
# Check packages
|
||||
dotnet list src/Umbraco.Cms.Api.Delivery/Umbraco.Cms.Api.Delivery.csproj package --outdated
|
||||
```
|
||||
|
||||
### Key Classes
|
||||
|
||||
| Class | Purpose | File |
|
||||
|-------|---------|------|
|
||||
| `DeliveryApiControllerBase` | Base controller with path decoding | Controllers/DeliveryApiControllerBase.cs |
|
||||
| `ApiContentQueryService` | Query orchestration | Services/ApiContentQueryService.cs |
|
||||
| `ApiAccessService` | Access control logic | Services/ApiAccessService.cs |
|
||||
| `DeliveryApiOutputCachePolicy` | Cache policy implementation | Caching/DeliveryApiOutputCachePolicy.cs |
|
||||
| `MemberController` | OAuth endpoints | Controllers/Security/MemberController.cs |
|
||||
| `RequestContextOutputExpansionStrategyV2` | V2 output expansion | Rendering/RequestContextOutputExpansionStrategyV2.cs |
|
||||
|
||||
### Important Files
|
||||
|
||||
- `Umbraco.Cms.Api.Delivery.csproj` - Project dependencies
|
||||
- `DependencyInjection/UmbracoBuilderExtensions.cs` - DI registration (lines 33-141)
|
||||
- `Configuration/DeliveryApiConfiguration.cs` - API constants
|
||||
- `Services/ApiContentQueryService.cs` - Query execution
|
||||
|
||||
### Getting Help
|
||||
|
||||
- **Root documentation**: `/CLAUDE.md` - Repository overview
|
||||
- **API Common patterns**: `/src/Umbraco.Cms.Api.Common/CLAUDE.md`
|
||||
- **Official docs**: https://docs.umbraco.com/umbraco-cms/reference/content-delivery-api
|
||||
- **Media docs**: https://docs.umbraco.com/umbraco-cms/reference/content-delivery-api/media-delivery-api
|
||||
|
||||
---
|
||||
|
||||
**This library exposes Umbraco content and media via REST for headless scenarios. Focus on query handlers, access control, and member authentication when working here.**
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Microsoft.OpenApi;
|
||||
using Microsoft.OpenApi.Models;
|
||||
using Swashbuckle.AspNetCore.SwaggerGen;
|
||||
using Umbraco.Cms.Api.Common.OpenApi;
|
||||
using Umbraco.Cms.Api.Delivery.Filters;
|
||||
|
||||
+22
-8
@@ -1,6 +1,6 @@
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Microsoft.OpenApi;
|
||||
using Microsoft.OpenApi.Models;
|
||||
using Swashbuckle.AspNetCore.SwaggerGen;
|
||||
using Umbraco.Cms.Api.Common.Security;
|
||||
using Umbraco.Cms.Api.Delivery.Controllers.Content;
|
||||
@@ -35,9 +35,23 @@ public class ConfigureUmbracoMemberAuthenticationDeliveryApiSwaggerGenOptions :
|
||||
return;
|
||||
}
|
||||
|
||||
var schemaRef = new OpenApiSecuritySchemeReference(AuthSchemeName, context.Document);
|
||||
operation.Security ??= new List<OpenApiSecurityRequirement>();
|
||||
operation.Security.Add(new OpenApiSecurityRequirement { [schemaRef] = [] });
|
||||
operation.Security = new List<OpenApiSecurityRequirement>
|
||||
{
|
||||
new OpenApiSecurityRequirement
|
||||
{
|
||||
{
|
||||
new OpenApiSecurityScheme
|
||||
{
|
||||
Reference = new OpenApiReference
|
||||
{
|
||||
Type = ReferenceType.SecurityScheme,
|
||||
Id = AuthSchemeName,
|
||||
}
|
||||
},
|
||||
[]
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public void Apply(OpenApiDocument swaggerDoc, DocumentFilterContext context)
|
||||
@@ -47,7 +61,7 @@ public class ConfigureUmbracoMemberAuthenticationDeliveryApiSwaggerGenOptions :
|
||||
return;
|
||||
}
|
||||
|
||||
swaggerDoc.AddComponent(
|
||||
swaggerDoc.Components.SecuritySchemes.Add(
|
||||
AuthSchemeName,
|
||||
new OpenApiSecurityScheme
|
||||
{
|
||||
@@ -60,9 +74,9 @@ public class ConfigureUmbracoMemberAuthenticationDeliveryApiSwaggerGenOptions :
|
||||
AuthorizationCode = new OpenApiOAuthFlow
|
||||
{
|
||||
AuthorizationUrl = new Uri(Paths.MemberApi.AuthorizationEndpoint, UriKind.Relative),
|
||||
TokenUrl = new Uri(Paths.MemberApi.TokenEndpoint, UriKind.Relative),
|
||||
},
|
||||
},
|
||||
TokenUrl = new Uri(Paths.MemberApi.TokenEndpoint, UriKind.Relative)
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
using System.Text.Json.Nodes;
|
||||
using Microsoft.OpenApi;
|
||||
using Microsoft.OpenApi.Any;
|
||||
using Microsoft.OpenApi.Models;
|
||||
using Swashbuckle.AspNetCore.SwaggerGen;
|
||||
using Umbraco.Cms.Api.Delivery.Configuration;
|
||||
using Umbraco.Cms.Api.Delivery.Controllers;
|
||||
using Umbraco.Cms.Api.Delivery.Controllers.Content;
|
||||
using Umbraco.Cms.Core;
|
||||
|
||||
namespace Umbraco.Cms.Api.Delivery.Filters;
|
||||
|
||||
@@ -13,7 +13,7 @@ internal sealed class SwaggerContentDocumentationFilter : SwaggerDocumentationFi
|
||||
|
||||
protected override void ApplyOperation(OpenApiOperation operation, OperationFilterContext context)
|
||||
{
|
||||
operation.Parameters ??= new List<IOpenApiParameter>();
|
||||
operation.Parameters ??= new List<OpenApiParameter>();
|
||||
|
||||
AddExpand(operation, context);
|
||||
|
||||
@@ -21,50 +21,50 @@ internal sealed class SwaggerContentDocumentationFilter : SwaggerDocumentationFi
|
||||
|
||||
operation.Parameters.Add(new OpenApiParameter
|
||||
{
|
||||
Name = Constants.DeliveryApi.HeaderNames.AcceptLanguage,
|
||||
Name = Core.Constants.DeliveryApi.HeaderNames.AcceptLanguage,
|
||||
In = ParameterLocation.Header,
|
||||
Required = false,
|
||||
Description = "Defines the language to return. Use this when querying language variant content items.",
|
||||
Schema = new OpenApiSchema { Type = JsonSchemaType.String },
|
||||
Examples = new Dictionary<string, IOpenApiExample>
|
||||
Schema = new OpenApiSchema { Type = "string" },
|
||||
Examples = new Dictionary<string, OpenApiExample>
|
||||
{
|
||||
{ "Default", new OpenApiExample { Value = string.Empty } },
|
||||
{ "English culture", new OpenApiExample { Value = "en-us" } },
|
||||
},
|
||||
{ "Default", new OpenApiExample { Value = new OpenApiString(string.Empty) } },
|
||||
{ "English culture", new OpenApiExample { Value = new OpenApiString("en-us") } }
|
||||
}
|
||||
});
|
||||
|
||||
operation.Parameters.Add(new OpenApiParameter
|
||||
{
|
||||
Name = Constants.DeliveryApi.HeaderNames.AcceptSegment,
|
||||
Name = Core.Constants.DeliveryApi.HeaderNames.AcceptSegment,
|
||||
In = ParameterLocation.Header,
|
||||
Required = false,
|
||||
Description = "Defines the segment to return. Use this when querying segment variant content items.",
|
||||
Schema = new OpenApiSchema { Type = JsonSchemaType.String },
|
||||
Examples = new Dictionary<string, IOpenApiExample>
|
||||
Schema = new OpenApiSchema { Type = "string" },
|
||||
Examples = new Dictionary<string, OpenApiExample>
|
||||
{
|
||||
{ "Default", new OpenApiExample { Value = string.Empty } },
|
||||
{ "Segment One", new OpenApiExample { Value = "segment-one" } },
|
||||
},
|
||||
{ "Default", new OpenApiExample { Value = new OpenApiString(string.Empty) } },
|
||||
{ "Segment One", new OpenApiExample { Value = new OpenApiString("segment-one") } }
|
||||
}
|
||||
});
|
||||
|
||||
AddApiKey(operation);
|
||||
|
||||
operation.Parameters.Add(new OpenApiParameter
|
||||
{
|
||||
Name = Constants.DeliveryApi.HeaderNames.Preview,
|
||||
Name = Core.Constants.DeliveryApi.HeaderNames.Preview,
|
||||
In = ParameterLocation.Header,
|
||||
Required = false,
|
||||
Description = "Whether to request draft content.",
|
||||
Schema = new OpenApiSchema { Type = JsonSchemaType.Boolean },
|
||||
Schema = new OpenApiSchema { Type = "boolean" }
|
||||
});
|
||||
|
||||
operation.Parameters.Add(new OpenApiParameter
|
||||
{
|
||||
Name = Constants.DeliveryApi.HeaderNames.StartItem,
|
||||
Name = Core.Constants.DeliveryApi.HeaderNames.StartItem,
|
||||
In = ParameterLocation.Header,
|
||||
Required = false,
|
||||
Description = "URL segment or GUID of a root content item.",
|
||||
Schema = new OpenApiSchema { Type = JsonSchemaType.String },
|
||||
Schema = new OpenApiSchema { Type = "string" }
|
||||
});
|
||||
}
|
||||
|
||||
@@ -92,36 +92,105 @@ internal sealed class SwaggerContentDocumentationFilter : SwaggerDocumentationFi
|
||||
}
|
||||
}
|
||||
|
||||
private Dictionary<string, IOpenApiExample> FetchQueryParameterExamples() =>
|
||||
private Dictionary<string, OpenApiExample> FetchQueryParameterExamples() =>
|
||||
new()
|
||||
{
|
||||
{ "Select all", new OpenApiExample { Value = string.Empty } },
|
||||
{ "Select all ancestors of a node by id", new OpenApiExample { Value = "ancestors:id" } },
|
||||
{ "Select all ancestors of a node by path", new OpenApiExample { Value = "ancestors:path" } },
|
||||
{ "Select all children of a node by id", new OpenApiExample { Value = "children:id" } },
|
||||
{ "Select all children of a node by path", new OpenApiExample { Value = "children:path" } },
|
||||
{ "Select all descendants of a node by id", new OpenApiExample { Value = "descendants:id" } },
|
||||
{ "Select all descendants of a node by path", new OpenApiExample { Value = "descendants:path" } },
|
||||
{ "Select all", new OpenApiExample { Value = new OpenApiString(string.Empty) } },
|
||||
{
|
||||
"Select all ancestors of a node by id",
|
||||
new OpenApiExample { Value = new OpenApiString("ancestors:id") }
|
||||
},
|
||||
{
|
||||
"Select all ancestors of a node by path",
|
||||
new OpenApiExample { Value = new OpenApiString("ancestors:path") }
|
||||
},
|
||||
{
|
||||
"Select all children of a node by id",
|
||||
new OpenApiExample { Value = new OpenApiString("children:id") }
|
||||
},
|
||||
{
|
||||
"Select all children of a node by path",
|
||||
new OpenApiExample { Value = new OpenApiString("children:path") }
|
||||
},
|
||||
{
|
||||
"Select all descendants of a node by id",
|
||||
new OpenApiExample { Value = new OpenApiString("descendants:id") }
|
||||
},
|
||||
{
|
||||
"Select all descendants of a node by path",
|
||||
new OpenApiExample { Value = new OpenApiString("descendants:path") }
|
||||
}
|
||||
};
|
||||
|
||||
private Dictionary<string, IOpenApiExample> FilterQueryParameterExamples() =>
|
||||
private Dictionary<string, OpenApiExample> FilterQueryParameterExamples() =>
|
||||
new()
|
||||
{
|
||||
{ "Default filter", new OpenApiExample { Value = string.Empty } },
|
||||
{ "Filter by content type (equals)", new OpenApiExample { Value = new JsonArray { "contentType:alias1" } } },
|
||||
{ "Filter by name (contains)", new OpenApiExample { Value = new JsonArray { "name:nodeName" } } },
|
||||
{ "Filter by creation date (less than)", new OpenApiExample { Value = new JsonArray { "createDate<2024-01-01" } } },
|
||||
{ "Filter by update date (greater than or equal)", new OpenApiExample { Value = new JsonArray { "updateDate>:2023-01-01" } } },
|
||||
{ "Default filter", new OpenApiExample { Value = new OpenApiString(string.Empty) } },
|
||||
{
|
||||
"Filter by content type (equals)",
|
||||
new OpenApiExample { Value = new OpenApiArray { new OpenApiString("contentType:alias1") } }
|
||||
},
|
||||
{
|
||||
"Filter by name (contains)",
|
||||
new OpenApiExample { Value = new OpenApiArray { new OpenApiString("name:nodeName") } }
|
||||
},
|
||||
{
|
||||
"Filter by creation date (less than)",
|
||||
new OpenApiExample { Value = new OpenApiArray { new OpenApiString("createDate<2024-01-01") } }
|
||||
},
|
||||
{
|
||||
"Filter by update date (greater than or equal)",
|
||||
new OpenApiExample { Value = new OpenApiArray { new OpenApiString("updateDate>:2023-01-01") } }
|
||||
}
|
||||
};
|
||||
|
||||
private Dictionary<string, IOpenApiExample> SortQueryParameterExamples() =>
|
||||
private Dictionary<string, OpenApiExample> SortQueryParameterExamples() =>
|
||||
new()
|
||||
{
|
||||
{ "Default sort", new OpenApiExample { Value = string.Empty } },
|
||||
{ "Sort by create date", new OpenApiExample { Value = new JsonArray { "createDate:asc", "createDate:desc" } } },
|
||||
{ "Sort by level", new OpenApiExample { Value = new JsonArray { "level:asc", "level:desc" } } },
|
||||
{ "Sort by name", new OpenApiExample { Value = new JsonArray { "name:asc", "name:desc" } } },
|
||||
{ "Sort by sort order", new OpenApiExample { Value = new JsonArray { "sortOrder:asc", "sortOrder:desc" } } },
|
||||
{ "Sort by update date", new OpenApiExample { Value = new JsonArray { "updateDate:asc", "updateDate:desc" } } },
|
||||
{ "Default sort", new OpenApiExample { Value = new OpenApiString(string.Empty) } },
|
||||
{
|
||||
"Sort by create date",
|
||||
new OpenApiExample
|
||||
{
|
||||
Value = new OpenApiArray
|
||||
{
|
||||
new OpenApiString("createDate:asc"), new OpenApiString("createDate:desc")
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"Sort by level",
|
||||
new OpenApiExample
|
||||
{
|
||||
Value = new OpenApiArray { new OpenApiString("level:asc"), new OpenApiString("level:desc") }
|
||||
}
|
||||
},
|
||||
{
|
||||
"Sort by name",
|
||||
new OpenApiExample
|
||||
{
|
||||
Value = new OpenApiArray { new OpenApiString("name:asc"), new OpenApiString("name:desc") }
|
||||
}
|
||||
},
|
||||
{
|
||||
"Sort by sort order",
|
||||
new OpenApiExample
|
||||
{
|
||||
Value = new OpenApiArray
|
||||
{
|
||||
new OpenApiString("sortOrder:asc"), new OpenApiString("sortOrder:desc")
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"Sort by update date",
|
||||
new OpenApiExample
|
||||
{
|
||||
Value = new OpenApiArray
|
||||
{
|
||||
new OpenApiString("updateDate:asc"), new OpenApiString("updateDate:desc")
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.OpenApi;
|
||||
using Microsoft.OpenApi.Any;
|
||||
using Microsoft.OpenApi.Models;
|
||||
using Swashbuckle.AspNetCore.SwaggerGen;
|
||||
using Umbraco.Cms.Core;
|
||||
|
||||
namespace Umbraco.Cms.Api.Delivery.Filters;
|
||||
|
||||
@@ -9,8 +9,6 @@ internal abstract class SwaggerDocumentationFilterBase<TBaseController>
|
||||
: SwaggerFilterBase<TBaseController>, IOperationFilter, IParameterFilter
|
||||
where TBaseController : Controller
|
||||
{
|
||||
protected abstract string DocumentationLink { get; }
|
||||
|
||||
public void Apply(OpenApiOperation operation, OperationFilterContext context)
|
||||
{
|
||||
if (CanApply(context))
|
||||
@@ -19,19 +17,21 @@ internal abstract class SwaggerDocumentationFilterBase<TBaseController>
|
||||
}
|
||||
}
|
||||
|
||||
public void Apply(IOpenApiParameter parameter, ParameterFilterContext context)
|
||||
public void Apply(OpenApiParameter parameter, ParameterFilterContext context)
|
||||
{
|
||||
if (CanApply(context) && parameter is OpenApiParameter openApiParameter)
|
||||
if (CanApply(context))
|
||||
{
|
||||
ApplyParameter(openApiParameter, context);
|
||||
ApplyParameter(parameter, context);
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract string DocumentationLink { get; }
|
||||
|
||||
protected abstract void ApplyOperation(OpenApiOperation operation, OperationFilterContext context);
|
||||
|
||||
protected abstract void ApplyParameter(OpenApiParameter parameter, ParameterFilterContext context);
|
||||
|
||||
protected void AddQueryParameterDocumentation(OpenApiParameter parameter, Dictionary<string, IOpenApiExample> examples, string description)
|
||||
protected void AddQueryParameterDocumentation(OpenApiParameter parameter, Dictionary<string, OpenApiExample> examples, string description)
|
||||
{
|
||||
parameter.Description = QueryParameterDescription(description);
|
||||
parameter.Examples = examples;
|
||||
@@ -60,19 +60,15 @@ internal abstract class SwaggerDocumentationFilterBase<TBaseController>
|
||||
AddFields(operation);
|
||||
}
|
||||
|
||||
protected void AddApiKey(OpenApiOperation operation)
|
||||
{
|
||||
operation.Parameters ??= new List<IOpenApiParameter>();
|
||||
operation.Parameters.Add(
|
||||
new OpenApiParameter
|
||||
{
|
||||
Name = Constants.DeliveryApi.HeaderNames.ApiKey,
|
||||
In = ParameterLocation.Header,
|
||||
Required = false,
|
||||
Description = "API key specified through configuration to authorize access to the API.",
|
||||
Schema = new OpenApiSchema { Type = JsonSchemaType.String },
|
||||
});
|
||||
}
|
||||
protected void AddApiKey(OpenApiOperation operation) =>
|
||||
operation.Parameters.Add(new OpenApiParameter
|
||||
{
|
||||
Name = Core.Constants.DeliveryApi.HeaderNames.ApiKey,
|
||||
In = ParameterLocation.Header,
|
||||
Required = false,
|
||||
Description = "API key specified through configuration to authorize access to the API.",
|
||||
Schema = new OpenApiSchema { Type = "string" }
|
||||
});
|
||||
|
||||
protected string PaginationDescription(bool skip, string itemType)
|
||||
=> $"Specifies the number of found {itemType} items to {(skip ? "skip" : "take")}. Use this to control pagination of the response.";
|
||||
@@ -86,70 +82,78 @@ internal abstract class SwaggerDocumentationFilterBase<TBaseController>
|
||||
|
||||
// FIXME: remove this when Delivery API V1 has been removed (expectedly in V15)
|
||||
private void AddExpandV1(OpenApiOperation operation)
|
||||
{
|
||||
operation.Parameters ??= new List<IOpenApiParameter>();
|
||||
operation.Parameters.Add(
|
||||
new OpenApiParameter
|
||||
=> operation.Parameters.Add(new OpenApiParameter
|
||||
{
|
||||
Name = "expand",
|
||||
In = ParameterLocation.Query,
|
||||
Required = false,
|
||||
Description = QueryParameterDescription("Defines the properties that should be expanded in the response"),
|
||||
Schema = new OpenApiSchema { Type = "string" },
|
||||
Examples = new Dictionary<string, OpenApiExample>
|
||||
{
|
||||
Name = "expand",
|
||||
In = ParameterLocation.Query,
|
||||
Required = false,
|
||||
Description =
|
||||
QueryParameterDescription("Defines the properties that should be expanded in the response"),
|
||||
Schema = new OpenApiSchema { Type = JsonSchemaType.String },
|
||||
Examples = new Dictionary<string, IOpenApiExample>
|
||||
{ "Expand none", new OpenApiExample { Value = new OpenApiString(string.Empty) } },
|
||||
{ "Expand all", new OpenApiExample { Value = new OpenApiString("all") } },
|
||||
{
|
||||
{ "Expand none", new OpenApiExample { Value = string.Empty } },
|
||||
{ "Expand all", new OpenApiExample { Value = "all" } },
|
||||
{ "Expand specific property", new OpenApiExample { Value = "property:alias1" } },
|
||||
{ "Expand specific properties", new OpenApiExample { Value = "property:alias1,alias2" } },
|
||||
"Expand specific property",
|
||||
new OpenApiExample { Value = new OpenApiString("property:alias1") }
|
||||
},
|
||||
});
|
||||
}
|
||||
{
|
||||
"Expand specific properties",
|
||||
new OpenApiExample { Value = new OpenApiString("property:alias1,alias2") }
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
private void AddExpand(OpenApiOperation operation)
|
||||
{
|
||||
operation.Parameters ??= new List<IOpenApiParameter>();
|
||||
operation.Parameters.Add(
|
||||
new OpenApiParameter
|
||||
=> operation.Parameters.Add(new OpenApiParameter
|
||||
{
|
||||
Name = "expand",
|
||||
In = ParameterLocation.Query,
|
||||
Required = false,
|
||||
Description = QueryParameterDescription("Defines the properties that should be expanded in the response"),
|
||||
Schema = new OpenApiSchema { Type = "string" },
|
||||
Examples = new Dictionary<string, OpenApiExample>
|
||||
{
|
||||
Name = "expand",
|
||||
In = ParameterLocation.Query,
|
||||
Required = false,
|
||||
Description =
|
||||
QueryParameterDescription("Defines the properties that should be expanded in the response"),
|
||||
Schema = new OpenApiSchema { Type = JsonSchemaType.String },
|
||||
Examples = new Dictionary<string, IOpenApiExample>
|
||||
{ "Expand none", new OpenApiExample { Value = new OpenApiString(string.Empty) } },
|
||||
{ "Expand all properties", new OpenApiExample { Value = new OpenApiString("properties[$all]") } },
|
||||
{
|
||||
{ "Expand none", new OpenApiExample { Value = string.Empty } },
|
||||
{ "Expand all properties", new OpenApiExample { Value = "properties[$all]" } },
|
||||
{ "Expand specific property", new OpenApiExample { Value = "properties[alias1]" } },
|
||||
{ "Expand specific properties", new OpenApiExample { Value = "properties[alias1,alias2]" } },
|
||||
{ "Expand nested properties", new OpenApiExample { Value = "properties[alias1[properties[nestedAlias1,nestedAlias2]]]" } },
|
||||
"Expand specific property",
|
||||
new OpenApiExample { Value = new OpenApiString("properties[alias1]") }
|
||||
},
|
||||
});
|
||||
}
|
||||
{
|
||||
"Expand specific properties",
|
||||
new OpenApiExample { Value = new OpenApiString("properties[alias1,alias2]") }
|
||||
},
|
||||
{
|
||||
"Expand nested properties",
|
||||
new OpenApiExample { Value = new OpenApiString("properties[alias1[properties[nestedAlias1,nestedAlias2]]]") }
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
private void AddFields(OpenApiOperation operation)
|
||||
{
|
||||
operation.Parameters ??= new List<IOpenApiParameter>();
|
||||
operation.Parameters.Add(
|
||||
new OpenApiParameter
|
||||
=> operation.Parameters.Add(new OpenApiParameter
|
||||
{
|
||||
Name = "fields",
|
||||
In = ParameterLocation.Query,
|
||||
Required = false,
|
||||
Description = QueryParameterDescription("Explicitly defines which properties should be included in the response (by default all properties are included)"),
|
||||
Schema = new OpenApiSchema { Type = "string" },
|
||||
Examples = new Dictionary<string, OpenApiExample>
|
||||
{
|
||||
Name = "fields",
|
||||
In = ParameterLocation.Query,
|
||||
Required = false,
|
||||
Description =
|
||||
QueryParameterDescription(
|
||||
"Explicitly defines which properties should be included in the response (by default all properties are included)"),
|
||||
Schema = new OpenApiSchema { Type = JsonSchemaType.String },
|
||||
Examples = new Dictionary<string, IOpenApiExample>
|
||||
{ "Include all properties", new OpenApiExample { Value = new OpenApiString("properties[$all]") } },
|
||||
{
|
||||
{ "Include all properties", new OpenApiExample { Value = "properties[$all]" } },
|
||||
{ "Include only specific property", new OpenApiExample { Value = "properties[alias1]" } },
|
||||
{ "Include only specific properties", new OpenApiExample { Value = "properties[alias1,alias2]" } },
|
||||
{ "Include only specific nested properties", new OpenApiExample { Value = "properties[alias1[properties[nestedAlias1,nestedAlias2]]]" } },
|
||||
"Include only specific property",
|
||||
new OpenApiExample { Value = new OpenApiString("properties[alias1]") }
|
||||
},
|
||||
});
|
||||
}
|
||||
{
|
||||
"Include only specific properties",
|
||||
new OpenApiExample { Value = new OpenApiString("properties[alias1,alias2]") }
|
||||
},
|
||||
{
|
||||
"Include only specific nested properties",
|
||||
new OpenApiExample { Value = new OpenApiString("properties[alias1[properties[nestedAlias1,nestedAlias2]]]") }
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
using System.Text.Json.Nodes;
|
||||
using Microsoft.OpenApi;
|
||||
using Microsoft.OpenApi.Any;
|
||||
using Microsoft.OpenApi.Models;
|
||||
using Swashbuckle.AspNetCore.SwaggerGen;
|
||||
using Umbraco.Cms.Api.Delivery.Configuration;
|
||||
using Umbraco.Cms.Api.Delivery.Controllers;
|
||||
using Umbraco.Cms.Api.Delivery.Controllers.Media;
|
||||
|
||||
namespace Umbraco.Cms.Api.Delivery.Filters;
|
||||
@@ -12,7 +13,7 @@ internal sealed class SwaggerMediaDocumentationFilter : SwaggerDocumentationFilt
|
||||
|
||||
protected override void ApplyOperation(OpenApiOperation operation, OperationFilterContext context)
|
||||
{
|
||||
operation.Parameters ??= new List<IOpenApiParameter>();
|
||||
operation.Parameters ??= new List<OpenApiParameter>();
|
||||
|
||||
AddExpand(operation, context);
|
||||
|
||||
@@ -45,29 +46,77 @@ internal sealed class SwaggerMediaDocumentationFilter : SwaggerDocumentationFilt
|
||||
}
|
||||
}
|
||||
|
||||
private Dictionary<string, IOpenApiExample> FetchQueryParameterExamples() =>
|
||||
private Dictionary<string, OpenApiExample> FetchQueryParameterExamples() =>
|
||||
new()
|
||||
{
|
||||
{ "Select all children at root level", new OpenApiExample { Value = "children:/" } },
|
||||
{ "Select all children of a media item by id", new OpenApiExample { Value = "children:id" } },
|
||||
{ "Select all children of a media item by path", new OpenApiExample { Value = "children:path" } },
|
||||
{
|
||||
"Select all children at root level",
|
||||
new OpenApiExample { Value = new OpenApiString("children:/") }
|
||||
},
|
||||
{
|
||||
"Select all children of a media item by id",
|
||||
new OpenApiExample { Value = new OpenApiString("children:id") }
|
||||
},
|
||||
{
|
||||
"Select all children of a media item by path",
|
||||
new OpenApiExample { Value = new OpenApiString("children:path") }
|
||||
}
|
||||
};
|
||||
|
||||
private Dictionary<string, IOpenApiExample> FilterQueryParameterExamples() =>
|
||||
private Dictionary<string, OpenApiExample> FilterQueryParameterExamples() =>
|
||||
new()
|
||||
{
|
||||
{ "Default filter", new OpenApiExample { Value = string.Empty } },
|
||||
{ "Filter by media type", new OpenApiExample { Value = new JsonArray { "mediaType:alias1" } } },
|
||||
{ "Filter by name", new OpenApiExample { Value = new JsonArray { "name:nodeName" } } },
|
||||
{ "Default filter", new OpenApiExample { Value = new OpenApiString(string.Empty) } },
|
||||
{
|
||||
"Filter by media type",
|
||||
new OpenApiExample { Value = new OpenApiArray { new OpenApiString("mediaType:alias1") } }
|
||||
},
|
||||
{
|
||||
"Filter by name",
|
||||
new OpenApiExample { Value = new OpenApiArray { new OpenApiString("name:nodeName") } }
|
||||
}
|
||||
};
|
||||
|
||||
private Dictionary<string, IOpenApiExample> SortQueryParameterExamples() =>
|
||||
private Dictionary<string, OpenApiExample> SortQueryParameterExamples() =>
|
||||
new()
|
||||
{
|
||||
{ "Default sort", new OpenApiExample { Value = string.Empty } },
|
||||
{ "Sort by create date", new OpenApiExample { Value = new JsonArray { "createDate:asc", "createDate:desc" } } },
|
||||
{ "Sort by name", new OpenApiExample { Value = new JsonArray { "name:asc", "name:desc" } } },
|
||||
{ "Sort by sort order", new OpenApiExample { Value = new JsonArray { "sortOrder:asc", "sortOrder:desc" } } },
|
||||
{ "Sort by update date", new OpenApiExample { Value = new JsonArray { "updateDate:asc", "updateDate:desc" } } },
|
||||
{ "Default sort", new OpenApiExample { Value = new OpenApiString(string.Empty) } },
|
||||
{
|
||||
"Sort by create date",
|
||||
new OpenApiExample
|
||||
{
|
||||
Value = new OpenApiArray
|
||||
{
|
||||
new OpenApiString("createDate:asc"), new OpenApiString("createDate:desc")
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"Sort by name",
|
||||
new OpenApiExample
|
||||
{
|
||||
Value = new OpenApiArray { new OpenApiString("name:asc"), new OpenApiString("name:desc") }
|
||||
}
|
||||
},
|
||||
{
|
||||
"Sort by sort order",
|
||||
new OpenApiExample
|
||||
{
|
||||
Value = new OpenApiArray
|
||||
{
|
||||
new OpenApiString("sortOrder:asc"), new OpenApiString("sortOrder:desc")
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"Sort by update date",
|
||||
new OpenApiExample
|
||||
{
|
||||
Value = new OpenApiArray
|
||||
{
|
||||
new OpenApiString("updateDate:asc"), new OpenApiString("updateDate:desc")
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
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;
|
||||
|
||||
namespace Umbraco.Cms.Api.Delivery.Querying;
|
||||
@@ -11,6 +14,30 @@ public abstract class QueryOptionBase
|
||||
private readonly IApiDocumentUrlService _apiDocumentUrlService;
|
||||
private readonly IVariationContextAccessor _variationContextAccessor;
|
||||
|
||||
[Obsolete("Please use the non-obsolete constructor. Will be removed in V17.")]
|
||||
public QueryOptionBase(
|
||||
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 QueryOptionBase(
|
||||
IPublishedContentCache publishedContentCache,
|
||||
IRequestRoutingService requestRoutingService,
|
||||
IRequestPreviewService requestPreviewService,
|
||||
IRequestCultureService requestCultureService,
|
||||
IApiDocumentUrlService apiDocumentUrlService,
|
||||
IVariationContextAccessor variationContextAccessor)
|
||||
: this(requestRoutingService, requestPreviewService, apiDocumentUrlService, variationContextAccessor)
|
||||
{
|
||||
}
|
||||
|
||||
public QueryOptionBase(
|
||||
IRequestRoutingService requestRoutingService,
|
||||
IRequestPreviewService requestPreviewService,
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
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.Cms.Core.Services.Navigation;
|
||||
|
||||
namespace Umbraco.Cms.Api.Delivery.Querying.Selectors;
|
||||
@@ -10,6 +13,46 @@ public sealed class AncestorsSelector : QueryOptionBase, ISelectorHandler
|
||||
private readonly IDocumentNavigationQueryService _navigationQueryService;
|
||||
private const string AncestorsSpecifier = "ancestors:";
|
||||
|
||||
[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)
|
||||
{
|
||||
}
|
||||
|
||||
[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,
|
||||
@@ -19,6 +62,18 @@ public sealed class AncestorsSelector : QueryOptionBase, ISelectorHandler
|
||||
: 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)
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool CanHandle(string query)
|
||||
=> query.StartsWith(AncestorsSpecifier, StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
@@ -12,6 +12,27 @@ 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,
|
||||
|
||||
@@ -12,6 +12,27 @@ 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,
|
||||
|
||||
@@ -66,14 +66,9 @@ internal sealed class DeliveryApiItemsEndpointsMatcherPolicy : MatcherPolicy, IE
|
||||
{
|
||||
ApiVersion[]? supportedApiVersions = endpoint.Metadata.GetMetadata<MapToApiVersionAttribute>()?.Versions.ToArray();
|
||||
|
||||
// If the endpoint is versioned, the requested API version must be among the API versions supported by the endpoint.
|
||||
// If the endpoint is NOT versioned, it cannot be used with a requested API version.
|
||||
if (supportedApiVersions is not null && requestedApiVersion is not null)
|
||||
{
|
||||
return supportedApiVersions.Contains(requestedApiVersion);
|
||||
}
|
||||
|
||||
return requestedApiVersion is null;
|
||||
// if the endpoint is versioned, the requested API version must be among the API versions supported by the endpoint.
|
||||
// if the endpoint is NOT versioned, it cannot be used with a requested API version
|
||||
return supportedApiVersions?.Contains(requestedApiVersion) ?? requestedApiVersion is null;
|
||||
}
|
||||
|
||||
private static bool IsByIdsController(ControllerActionDescriptor? controllerActionDescriptor)
|
||||
|
||||
@@ -66,7 +66,7 @@ internal sealed class ApiMediaQueryService : IApiMediaQueryService
|
||||
private IPublishedContent? TryGetByPath(string path, IPublishedMediaCache mediaCache)
|
||||
{
|
||||
var segments = path.Split(Constants.CharArrays.ForwardSlash, StringSplitOptions.RemoveEmptyEntries);
|
||||
IEnumerable<IPublishedContent> currentChildren = GetRootContent(mediaCache);
|
||||
IEnumerable<IPublishedContent> currentChildren = mediaCache.GetAtRoot();
|
||||
IPublishedContent? resolvedMedia = null;
|
||||
|
||||
foreach (var segment in segments)
|
||||
@@ -103,7 +103,7 @@ internal sealed class ApiMediaQueryService : IApiMediaQueryService
|
||||
IPublishedMediaCache mediaCache = GetRequiredPublishedMediaCache();
|
||||
if (childrenOf.Trim(Constants.CharArrays.ForwardSlash).Length == 0)
|
||||
{
|
||||
return GetRootContent(mediaCache);
|
||||
return mediaCache.GetAtRoot();
|
||||
}
|
||||
|
||||
IPublishedContent? parent = Guid.TryParse(childrenOf, out Guid parentKey)
|
||||
@@ -196,8 +196,4 @@ internal sealed class ApiMediaQueryService : IApiMediaQueryService
|
||||
|
||||
return Attempt.SucceedWithStatus(ApiMediaQueryOperationStatus.Success, result);
|
||||
}
|
||||
|
||||
private IEnumerable<IPublishedContent> GetRootContent(IPublishedMediaCache mediaCache)
|
||||
=> _mediaNavigationQueryService.TryGetRootKeys(out IEnumerable<Guid> rootKeys) is false ? []
|
||||
: rootKeys.Select(x => mediaCache.GetById(false, x)).WhereNotNull();
|
||||
}
|
||||
|
||||
@@ -19,6 +19,12 @@ internal sealed partial class RequestCultureService : RequestHeaderHandler, IReq
|
||||
return ValidLanguageHeaderRegex().IsMatch(acceptLanguage) ? acceptLanguage : null;
|
||||
}
|
||||
|
||||
[Obsolete("Use IVariationContextAccessor to manipulate the variation context. Scheduled for removal in V17.")]
|
||||
public void SetRequestCulture(string culture)
|
||||
{
|
||||
// no-op
|
||||
}
|
||||
|
||||
// at the time of writing we're introducing this to get rid of accept-language header values like "en-GB,en-US;q=0.9,en;q=0.8",
|
||||
// so we don't want to be too restrictive in this regex - keep it simple for now.
|
||||
[GeneratedRegex(@"^[\w-]*$")]
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
<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:
|
||||
@@ -12,18 +11,11 @@
|
||||
-->
|
||||
<WarningsNotAsErrors>$(WarningsNotAsErrors),ASP0019,CS0618,CS0612</WarningsNotAsErrors>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Umbraco.Cms.Api.Common\Umbraco.Cms.Api.Common.csproj" />
|
||||
<ProjectReference Include="..\Umbraco.Web.Common\Umbraco.Web.Common.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="System.Linq.Async">
|
||||
<ExcludeAssets>compile</ExcludeAssets>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleTo">
|
||||
<_Parameter1>Umbraco.Tests.UnitTests</_Parameter1>
|
||||
|
||||
@@ -1,621 +0,0 @@
|
||||
# Umbraco CMS - Management API
|
||||
|
||||
RESTful API for Umbraco backoffice operations. Manages content, media, users, and system configuration through OpenAPI-documented endpoints.
|
||||
|
||||
**Project**: `Umbraco.Cms.Api.Management`
|
||||
**Type**: ASP.NET Core Web API Library
|
||||
**Files**: 1,317 C# files across 54+ controller domains
|
||||
|
||||
---
|
||||
|
||||
## 1. Architecture
|
||||
|
||||
### Target Framework
|
||||
- **.NET 10.0** (`net10.0`)
|
||||
- **C# 12** with nullable reference types enabled
|
||||
- **ASP.NET Core** Web API
|
||||
|
||||
### Application Type
|
||||
**REST API Library** - Plugged into Umbraco.Web.UI, provides the Management API surface for backoffice operations.
|
||||
|
||||
### Key Technologies
|
||||
- **Web Framework**: ASP.NET Core MVC with `Asp.Versioning.Mvc` (v1.0 currently)
|
||||
- **OpenAPI**: Swashbuckle.AspNetCore with custom schema/operation filters
|
||||
- **Authentication**: OpenIddict via `Umbraco.Cms.Api.Common` (reference tokens, not JWT)
|
||||
- **Authorization**: Policy-based with `IAuthorizationService`
|
||||
- **Validation**: FluentValidation via base controllers
|
||||
- **Serialization**: System.Text.Json with custom converters
|
||||
- **Mapping**: Manual presentation factories (no AutoMapper)
|
||||
- **Patching**: JsonPatch.Net for PATCH operations
|
||||
- **Real-time**: SignalR hubs (`BackofficeHub`, `ServerEventHub`)
|
||||
- **DI**: Microsoft.Extensions.DependencyInjection via `ManagementApiComposer`
|
||||
|
||||
### Project Structure
|
||||
```
|
||||
src/Umbraco.Cms.Api.Management/
|
||||
├── Controllers/ # 54+ domain-specific controller folders
|
||||
│ ├── Document/ # Document (content) CRUD + publish/unpublish
|
||||
│ ├── Media/ # Media CRUD + upload
|
||||
│ ├── Member/ # Member management
|
||||
│ ├── User/ # User management
|
||||
│ ├── DataType/ # Data type configuration
|
||||
│ ├── DocumentType/ # Content type schemas
|
||||
│ ├── Template/ # Razor template management
|
||||
│ ├── Dictionary/ # Localization dictionary
|
||||
│ ├── Language/ # Language/culture config
|
||||
│ ├── Security/ # Auth, login, external logins
|
||||
│ ├── Install/ # Installation wizard
|
||||
│ ├── Upgrade/ # Upgrade operations
|
||||
│ ├── LogViewer/ # Log browsing
|
||||
│ ├── HealthCheck/ # Health check dashboard
|
||||
│ ├── Webhook/ # Webhook management
|
||||
│ └── [48 more domains...]
|
||||
│
|
||||
├── ViewModels/ # Request/response DTOs (one folder per domain)
|
||||
├── Factories/ # Domain model → ViewModel converters
|
||||
├── Services/ # Business logic (thin layer over Core.Services)
|
||||
├── Mapping/ # ViewModel → domain model mappers
|
||||
├── Security/ # Auth providers, sign-in manager, external logins
|
||||
├── OpenApi/ # Swashbuckle filters (schema, operation, security)
|
||||
├── Routing/ # Route configuration, SignalR hubs
|
||||
├── DependencyInjection/ # Service registration (55+ files)
|
||||
├── Middleware/ # Preview, server events
|
||||
├── Configuration/ # IOptions configurators
|
||||
├── Filters/ # Action filters
|
||||
├── Serialization/ # JSON converters
|
||||
└── OpenApi.json # Embedded OpenAPI spec (1.3MB)
|
||||
```
|
||||
|
||||
### Dependencies
|
||||
- **Umbraco.Cms.Api.Common** - Shared API infrastructure (base controllers, OpenAPI config)
|
||||
- **Umbraco.Infrastructure** - Service implementations, data access
|
||||
- **Umbraco.PublishedCache.HybridCache** - Published content queries
|
||||
- **JsonPatch.Net** - JSON Patch (RFC 6902) support
|
||||
- **Swashbuckle.AspNetCore** - OpenAPI generation
|
||||
|
||||
### Design Patterns
|
||||
1. **Controller-per-Operation** - Each endpoint is a separate controller class
|
||||
- Example: `CreateDocumentController`, `UpdateDocumentController`, `DeleteDocumentController`
|
||||
- Enables fine-grained authorization and operation-specific logic
|
||||
- **Responsibilities**: entrypoint/routing, authorization and mapping
|
||||
- **avoid**: business logic directly in controllers (there are a few known violations)
|
||||
|
||||
2. **Presentation Factory Pattern** - Factories convert domain models to ViewModels
|
||||
- Example: `IDocumentEditingPresentationFactory` (src/Umbraco.Cms.Api.Management/Factories/)
|
||||
- Separation: Controllers → Factories → ViewModels
|
||||
|
||||
3. **Attempt Pattern** - Operations return `Attempt<TResult, TStatus>` for status-based error handling
|
||||
- Controllers map status enums to HTTP status codes via helper methods
|
||||
|
||||
4. **Authorization Service Pattern** - All authorization via `IAuthorizationService`, not attributes
|
||||
- Checked in base controller methods (see `ManagementApiControllerBase`)
|
||||
|
||||
5. **Options Pattern** - All configuration via `IOptions<T>` (security, routing, OpenAPI)
|
||||
|
||||
6. **SignalR Event Broadcasting** - Real-time notifications via `BackofficeHub` and `ServerEventHub`
|
||||
|
||||
---
|
||||
|
||||
## 2. Commands
|
||||
|
||||
### Build & Run
|
||||
```bash
|
||||
# Build
|
||||
dotnet build src/Umbraco.Cms.Api.Management
|
||||
|
||||
# Test (tests in ../../tests/Umbraco.Tests.Integration and Umbraco.Tests.UnitTests)
|
||||
dotnet test --filter "FullyQualifiedName~Management"
|
||||
|
||||
# Pack (for NuGet distribution)
|
||||
dotnet pack src/Umbraco.Cms.Api.Management -c Release
|
||||
```
|
||||
|
||||
### Code Quality
|
||||
```bash
|
||||
# Format code
|
||||
dotnet format src/Umbraco.Cms.Api.Management
|
||||
|
||||
# Build with warnings (note: some warnings suppressed, see .csproj line 23)
|
||||
dotnet build src/Umbraco.Cms.Api.Management /p:TreatWarningsAsErrors=true
|
||||
```
|
||||
|
||||
### OpenAPI Documentation
|
||||
The project embeds a pre-generated `OpenApi.json` (1.3MB). To regenerate:
|
||||
```bash
|
||||
# Run Umbraco.Web.UI, access /umbraco/swagger
|
||||
# Export JSON from Swagger UI
|
||||
```
|
||||
|
||||
### Package Management
|
||||
```bash
|
||||
# Add package (versions centralized in Directory.Packages.props)
|
||||
dotnet add src/Umbraco.Cms.Api.Management package [PackageName]
|
||||
|
||||
# Check for vulnerable packages
|
||||
dotnet list src/Umbraco.Cms.Api.Management package --vulnerable
|
||||
```
|
||||
|
||||
### Environment Setup
|
||||
1. **Prerequisites**: .NET 10 SDK
|
||||
2. **IDE**: Visual Studio 2022 or Rider (with .editorconfig support)
|
||||
3. **Configuration**: Inherits from `Umbraco.Web.UI` appsettings (no app settings in this library)
|
||||
|
||||
---
|
||||
|
||||
## 3. Style Guide
|
||||
|
||||
### Project-Specific Patterns
|
||||
|
||||
**Controller Naming** (line examples from CreateDocumentController.cs:16):
|
||||
```csharp
|
||||
[ApiVersion("1.0")]
|
||||
public class CreateDocumentController : CreateDocumentControllerBase
|
||||
```
|
||||
- Pattern: `{Verb}{Entity}Controller` (e.g., `CreateDocumentController`, `UpdateMediaController`)
|
||||
- Base class: `{Verb}{Entity}ControllerBase` for shared logic
|
||||
- **Critical**: One operation per controller (not one controller per resource)
|
||||
|
||||
**Async Naming** - All async methods use `Async` suffix consistently:
|
||||
```csharp
|
||||
await _contentEditingService.CreateAsync(model, CurrentUserKey(_backOfficeSecurityAccessor));
|
||||
```
|
||||
|
||||
**Factory Pattern Usage** (line 44):
|
||||
```csharp
|
||||
ContentCreateModel model = _documentEditingPresentationFactory.MapCreateModel(requestModel);
|
||||
```
|
||||
- ViewModels → Domain: `Map{Operation}Model(requestModel)`
|
||||
- Domain → ViewModels: Factory classes in `Factories/` folder
|
||||
|
||||
### Key Patterns from Codebase
|
||||
|
||||
**ControllerBase Helper Methods** (inherited from `ManagementApiControllerBase` in Api.Common):
|
||||
- `CreatedAtId<TController>(expression, id)` - Returns 201 with Location header
|
||||
- `ContentEditingOperationStatusResult(status)` - Maps status enum to ProblemDetails
|
||||
- `CurrentUserKey(accessor)` - Gets current user from security context
|
||||
|
||||
**Authorization Pattern** (all controllers):
|
||||
```csharp
|
||||
private readonly IAuthorizationService _authorizationService;
|
||||
// Check permissions in action, not via [Authorize] attribute
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Test Bench
|
||||
|
||||
### Test Location
|
||||
- **Unit Tests**: `tests/Umbraco.Tests.UnitTests/Umbraco.Cms.Api.Management/`
|
||||
- **Integration Tests**: `tests/Umbraco.Tests.Integration/Umbraco.Cms.Api.Management/`
|
||||
|
||||
### Running Tests
|
||||
```bash
|
||||
# All Management API tests
|
||||
dotnet test --filter "FullyQualifiedName~Management"
|
||||
|
||||
# Specific domain (e.g., Document controllers)
|
||||
dotnet test --filter "FullyQualifiedName~Management.Controllers.Document"
|
||||
```
|
||||
|
||||
### Testing Focus
|
||||
1. **Controller logic** - Request validation, authorization checks, status code mapping
|
||||
2. **Factories** - ViewModel ↔ Domain model conversion accuracy
|
||||
3. **Authorization** - Policy enforcement for each operation
|
||||
4. **OpenAPI schema** - Ensure Swagger generation doesn't break
|
||||
|
||||
### InternalsVisibleTo
|
||||
Tests have access to internal types (see .csproj:44-52):
|
||||
- `Umbraco.Tests.UnitTests`
|
||||
- `Umbraco.Tests.Integration`
|
||||
- `DynamicProxyGenAssembly2` (for Moq)
|
||||
|
||||
---
|
||||
|
||||
## 5. Error Handling
|
||||
|
||||
### Operation Status Pattern
|
||||
Controllers use `Attempt<TResult, TStatus>` with strongly-typed status enums:
|
||||
```csharp
|
||||
Attempt<ContentCreateResult, ContentEditingOperationStatus> result =
|
||||
await _contentEditingService.CreateAsync(model, userKey);
|
||||
|
||||
return result.Success
|
||||
? CreatedAtId<ByKeyDocumentController>(controller => nameof(controller.ByKey), result.Result.Content!.Key)
|
||||
: ContentEditingOperationStatusResult(result.Status); // Maps to ProblemDetails
|
||||
```
|
||||
|
||||
**Status Enums** (from Core):
|
||||
- `ContentEditingOperationStatus` - InvalidParent, NotFound, NotAllowed, etc.
|
||||
- `UserOperationStatus` - UserNameIsNotEmail, DuplicateUserName, etc.
|
||||
- Each enum value maps to specific HTTP status + ProblemDetails type
|
||||
|
||||
### ProblemDetails
|
||||
All errors return RFC 7807 ProblemDetails via helper methods in base controllers:
|
||||
- 400 Bad Request: Validation failures, invalid operations
|
||||
- 403 Forbidden: Authorization failures
|
||||
- 404 Not Found: Resource not found
|
||||
- 409 Conflict: Duplicate operations
|
||||
|
||||
### Critical Logging Points
|
||||
1. **Authorization failures** - Logged by AuthorizationService
|
||||
2. **Service operation failures** - Logged in Infrastructure layer services
|
||||
3. **External login errors** - BackOfficeSignInManager (Security/)
|
||||
|
||||
---
|
||||
|
||||
## 6. Clean Code
|
||||
|
||||
### Key Design Decisions
|
||||
|
||||
**Why Controller-per-Operation?** (not RESTful resource-based controllers)
|
||||
- Fine-grained authorization per operation
|
||||
- Operation-specific request/response models
|
||||
- Clearer OpenAPI documentation
|
||||
- Example: 20+ controllers in `Controllers/Document/` for different operations
|
||||
|
||||
**Why Manual Factories instead of AutoMapper?**
|
||||
- Explicit control over mapping logic
|
||||
- Easier debugging (no magic)
|
||||
- Better performance (no reflection overhead)
|
||||
- See `Factories/` directory (92 factory classes)
|
||||
|
||||
### Project-Specific Architectural Decisions
|
||||
|
||||
**Embedded OpenAPI Spec** (OpenApi.json - 1.3MB):
|
||||
- Pre-generated, embedded as resource
|
||||
- Served for client SDK generation
|
||||
- **Why?** Deterministic output, faster startup (no runtime generation)
|
||||
|
||||
**SignalR for Real-time** (Routing/BackofficeHub.cs:33):
|
||||
- `BackofficeHub` - User notifications, cache refreshes
|
||||
- `ServerEventHub` - Background job updates, health checks
|
||||
- Routes: `/umbraco/backoffice-signalr`, `/umbraco/serverevent-signalr`
|
||||
|
||||
### Code Smells to Watch For
|
||||
|
||||
1. **Large factory classes** - Some factories have 1000+ lines (e.g., `UserGroupPresentationFactory.cs`)
|
||||
- Consider splitting by operation
|
||||
|
||||
2. **Repeated authorization checks** - Each controller duplicates auth logic
|
||||
- Already abstracted to base classes, but still verbose
|
||||
|
||||
3. **ViewModel explosion** - 1000+ ViewModel classes across ViewModels/ folders
|
||||
- Consider shared base models or composition
|
||||
|
||||
---
|
||||
|
||||
## 7. Security
|
||||
|
||||
### Authentication & Authorization
|
||||
**Method**: OpenIddict (OAuth 2.0) via Umbraco.Cms.Api.Common
|
||||
- Reference tokens (not JWT) stored in database
|
||||
- Token validation via OpenIddict middleware
|
||||
- ASP.NET Core Data Protection for token encryption
|
||||
|
||||
**Authorization**:
|
||||
- Basic authorization is done trough policies and the `AuthorizeAttribute`
|
||||
```csharp
|
||||
// Example from DocumentTreeControllerBase.cs, the user needs at least access to a section that uses trees
|
||||
[Authorize(Policy = AuthorizationPolicies.SectionAccessForContentTree)]
|
||||
```
|
||||
- Authorization that needs (parts of) the payload are done manually trough the IAuthorizationService
|
||||
```csharp
|
||||
// Example from CreateDocumentController.cs:22
|
||||
private readonly IAuthorizationService _authorizationService;
|
||||
|
||||
// Authorization checked in base controller methods, not attributes
|
||||
protected async Task<IActionResult> HandleRequest(request, Func<Task<IActionResult>> handler)
|
||||
{
|
||||
var authResult = await _authorizationService.AuthorizeAsync(User, request, policy);
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
**Policies** (defined in Security/Authorization/):
|
||||
- `ContentPermissionHandler` - Document/Media CRUD permissions
|
||||
- `SectionAccessHandler` - Backoffice section access
|
||||
- `UserGroupPermissionHandler` - Admin operations
|
||||
|
||||
**Password Requirements** (Security/ConfigureBackOfficeIdentityOptions.cs:18):
|
||||
- See Identity options configuration
|
||||
|
||||
### External Login Providers
|
||||
**Location**: `Security/BackOfficeExternalLoginProviders.cs`
|
||||
- Google, Microsoft, OpenID Connect providers
|
||||
- Auto-linking with `ExternalSignInAutoLinkOptions`
|
||||
- **Critical**: Validate external claims before auto-linking users
|
||||
|
||||
### Input Validation
|
||||
**FluentValidation** used throughout:
|
||||
- Request models validated automatically via MVC integration
|
||||
- Custom validators in each domain folder (e.g., `ViewModels/Document/Validators/`)
|
||||
|
||||
**Parameter Validation**:
|
||||
```csharp
|
||||
// Controllers validate IDs, keys before service calls
|
||||
if (requestModel.Parent == null)
|
||||
return BadRequest(new ProblemDetailsBuilder()...);
|
||||
```
|
||||
|
||||
### API Security
|
||||
**CORS** - Configured in Umbraco.Web.UI (not this project)
|
||||
|
||||
**HTTPS Enforcement** - Configured in Umbraco.Web.UI
|
||||
|
||||
**Request Size Limits** - Configured for file uploads in Umbraco.Web.UI
|
||||
|
||||
**Security Headers** - Handled by Umbraco.Web.UI middleware
|
||||
|
||||
### Secrets Management
|
||||
**No secrets in this project** - Configuration injected from parent application (Umbraco.Web.UI)
|
||||
|
||||
### Dependency Security
|
||||
```bash
|
||||
# Check vulnerable packages
|
||||
dotnet list src/Umbraco.Cms.Api.Management package --vulnerable
|
||||
```
|
||||
|
||||
### Security Anti-Patterns to Avoid
|
||||
1. **Never bypass authorization checks** - All operations must authorize
|
||||
2. **Never trust client validation** - Always validate on server
|
||||
3. **Never expose stack traces** - ProblemDetails abstracts errors
|
||||
4. **Never log sensitive data** - User passwords, tokens, API keys
|
||||
|
||||
---
|
||||
|
||||
## 8. Teamwork and Workflow
|
||||
|
||||
**⚠️ SKIPPED** - This is a sub-project. See root `/CLAUDE.md` for repository-wide teamwork protocols.
|
||||
|
||||
---
|
||||
|
||||
## 9. Edge Cases
|
||||
|
||||
### Domain-Specific Edge Cases
|
||||
|
||||
**Document Operations**:
|
||||
1. **Publishing with descendants** - Can timeout on large trees
|
||||
- Use `PublishDocumentWithDescendantsController` with result polling
|
||||
- See `Controllers/Document/PublishDocumentWithDescendantsResultController.cs`
|
||||
|
||||
2. **Recycle bin operations** - Items in recycle bin can't be published
|
||||
- Check `IsTrashed` before publish operations
|
||||
|
||||
3. **Public access rules** - Affects authorization and routing
|
||||
- See `Controllers/Document/CreatePublicAccessDocumentController.cs`
|
||||
|
||||
**Media Upload**:
|
||||
1. **Large file uploads** - Request size limits in parent app
|
||||
- Controllers accept multipart/form-data
|
||||
- Temporary files cleaned by background job
|
||||
|
||||
2. **Media picker** - Can reference deleted media
|
||||
- Validation in `Factories/` checks for orphaned references
|
||||
|
||||
**User Management**:
|
||||
1. **External logins** - Auto-linking can create duplicate users if email mismatches
|
||||
- See `Security/ExternalSignInAutoLinkOptions.cs`
|
||||
|
||||
2. **User groups** - Deleting user group doesn't delete users
|
||||
- Users reassigned to default group
|
||||
|
||||
**Webhooks**:
|
||||
1. **Webhook failures** - Failed webhooks retry with exponential backoff
|
||||
- See `Controllers/Webhook/` for configuration
|
||||
|
||||
### Known Gotchas (from TODO comments)
|
||||
|
||||
**StyleSheet/Script/PartialView Tree Controllers** - All have identical TODO comment:
|
||||
```
|
||||
TODO: [NL] This must return path segments for a query to work
|
||||
// src/Umbraco.Cms.Api.Management/Controllers/Stylesheet/Tree/StylesheetTreeControllerBase.cs
|
||||
// src/Umbraco.Cms.Api.Management/Controllers/Script/Tree/ScriptTreeControllerBase.cs
|
||||
// src/Umbraco.Cms.Api.Management/Controllers/PartialView/Tree/PartialViewTreeControllerBase.cs
|
||||
```
|
||||
|
||||
**BackOfficeController** - External login TODO:
|
||||
```
|
||||
// src/Umbraco.Cms.Api.Management/Controllers/Security/BackOfficeController.cs
|
||||
// TODO: Handle external logins properly
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 10. Agentic Workflow
|
||||
|
||||
### When to Add a New Endpoint
|
||||
|
||||
**Decision Points**:
|
||||
1. Does the operation fit an existing controller domain? (Document, Media, User, etc.)
|
||||
2. Is this a new CRUD operation or a custom action?
|
||||
3. Does this require new authorization policies?
|
||||
|
||||
**Workflow**:
|
||||
1. **Create Controller** in appropriate `Controllers/{Domain}/` folder
|
||||
- Follow naming: `{Verb}{Entity}Controller`
|
||||
- Inherit from domain-specific base controller or `ManagementApiControllerBase`
|
||||
|
||||
2. **Define ViewModels** in `ViewModels/{Domain}/`
|
||||
- Request model (e.g., `CreateDocumentRequestModel`)
|
||||
- Response model (if not reusing existing)
|
||||
|
||||
3. **Create or Update Factory** in `Factories/`
|
||||
- Map request ViewModel → domain model
|
||||
- Map domain result → response ViewModel
|
||||
|
||||
4. **Authorization** - Check required permissions in controller action
|
||||
- Use `IAuthorizationService.AuthorizeAsync()`
|
||||
|
||||
5. **Service Layer** - Call Core services (from `Umbraco.Core.Services`)
|
||||
- Handle `Attempt<>` results
|
||||
- Map status to HTTP status codes
|
||||
|
||||
6. **OpenAPI Annotations**:
|
||||
- `[ApiVersion("1.0")]`
|
||||
- `[MapToApiVersion("1.0")]`
|
||||
- `[ProducesResponseType(...)]` for all status codes
|
||||
|
||||
7. **Testing**:
|
||||
- Unit test controller logic
|
||||
- Integration test end-to-end flow
|
||||
|
||||
8. **Update OpenApi.json** (if needed for client generation)
|
||||
|
||||
### Quality Gates Before PR
|
||||
1. All tests pass
|
||||
2. Code formatted (`dotnet format`)
|
||||
3. No new warnings (check suppressed warnings list in .csproj:23)
|
||||
4. OpenAPI schema valid (run Swagger UI)
|
||||
5. Authorization tested (unit + integration tests)
|
||||
|
||||
### Common Pitfalls
|
||||
1. **Forgetting authorization checks** - Every operation must authorize
|
||||
2. **Inconsistent status code mapping** - Use base controller helpers
|
||||
3. **Large factory classes** - Split by operation if >500 lines
|
||||
4. **Missing ProducesResponseType** - Breaks OpenAPI client generation
|
||||
5. **Not handling Attempt failures** - Always check `result.Success`
|
||||
|
||||
---
|
||||
|
||||
## 11. Project-Specific Notes
|
||||
|
||||
### Key Design Decisions
|
||||
|
||||
**Why 1,317 files for one API?**
|
||||
- Controller-per-operation pattern = many controllers
|
||||
- 54 domains × average 10-20 operations per domain
|
||||
- Tradeoff: Verbose but explicit, easier to navigate than megacontrollers
|
||||
|
||||
**Why manual factories instead of AutoMapper?**
|
||||
- Performance: No reflection overhead
|
||||
- Debuggability: Step through mapping logic
|
||||
- Control: Complex mappings (e.g., security trimming) require custom logic
|
||||
|
||||
**Why embedded OpenApi.json?**
|
||||
- Deterministic OpenAPI spec for client generation
|
||||
- Faster startup (no runtime generation)
|
||||
- Easier versioning (commit changes to spec)
|
||||
- Added `OpenAPIContractTest` to test that all operations are exported
|
||||
|
||||
### External Integrations
|
||||
None directly in this project. All integrations handled by:
|
||||
- **Umbraco.Infrastructure** - External search, media, email providers
|
||||
- **Umbraco.Core** - External data sources, webhooks
|
||||
|
||||
### Known Limitations
|
||||
|
||||
1. **API Versioning**: Currently v1.0 and v1.1
|
||||
- Future versions will require new controller classes or action methods
|
||||
|
||||
2. **Batch Operations**: Limited batch endpoint support
|
||||
- Most operations are single-entity (create one document at a time)
|
||||
|
||||
3. **Real-time Limits**: SignalR hubs don't scale beyond single-server without Redis backplane
|
||||
- Configure Redis for multi-server setups
|
||||
|
||||
4. **File Upload Size**: Controlled by parent app (Umbraco.Web.UI)
|
||||
- This project doesn't set limits
|
||||
|
||||
### Performance Considerations
|
||||
|
||||
**Caching**:
|
||||
- Published content cached via `Umbraco.PublishedCache.HybridCache`
|
||||
- Controllers query cache, not database (for published content)
|
||||
|
||||
**Background Jobs**:
|
||||
- Long-running operations (publish with descendants, export) return job ID
|
||||
- Poll result endpoint for completion
|
||||
- See `Controllers/Document/PublishDocumentWithDescendantsResultController.cs`
|
||||
|
||||
**OpenAPI Generation**:
|
||||
- Pre-generated (OpenApi.json embedded)
|
||||
- Runtime generation disabled for performance
|
||||
|
||||
### Technical Debt (Top Issues from TODO comments)
|
||||
|
||||
1. **Warnings Suppressed** (Umbraco.Cms.Api.Management.csproj:9-22):
|
||||
```
|
||||
TODO: Fix and remove overrides:
|
||||
- SA1117: params all on same line
|
||||
- SA1401: make fields private
|
||||
- SA1134: own line attributes
|
||||
- CS0108: hidden inherited member
|
||||
- CS0618/CS9042: update obsolete references
|
||||
- CS1998: remove async or make method synchronous
|
||||
- CS8524: switch statement exhaustiveness
|
||||
- IDE0060: removed unused parameter
|
||||
- SA1649: file name match type
|
||||
- CS0419: ambiguous reference
|
||||
- CS1573: param tag for all parameters
|
||||
- CS1574: unresolveable cref
|
||||
```
|
||||
|
||||
2. **Tree Controller Path Segments** (multiple files):
|
||||
- Stylesheet/Script/PartialView tree controllers need path segment support for queries
|
||||
- Files: `Controllers/Stylesheet/Tree/StylesheetTreeControllerBase.cs`
|
||||
- Files: `Controllers/Script/Tree/ScriptTreeControllerBase.cs`
|
||||
- Files: `Controllers/PartialView/Tree/PartialViewTreeControllerBase.cs`
|
||||
|
||||
3. **External Login Handling** (Controllers/Security/BackOfficeController.cs):
|
||||
- TODO: Handle external logins properly
|
||||
|
||||
4. **Large Factory Classes** (Factories/UserGroupPresentationFactory.cs):
|
||||
- Some factories exceed 1000 lines - consider splitting
|
||||
|
||||
5. **ViewModel Explosion** - 1000+ ViewModel classes
|
||||
- Consider shared base models or composition to reduce duplication
|
||||
|
||||
---
|
||||
|
||||
## Quick Reference
|
||||
|
||||
### Essential Commands
|
||||
```bash
|
||||
# Build
|
||||
dotnet build src/Umbraco.Cms.Api.Management
|
||||
|
||||
# Test Management API
|
||||
dotnet test --filter "FullyQualifiedName~Management"
|
||||
|
||||
# Format code
|
||||
dotnet format src/Umbraco.Cms.Api.Management
|
||||
|
||||
# Pack for NuGet
|
||||
dotnet pack src/Umbraco.Cms.Api.Management -c Release
|
||||
```
|
||||
|
||||
### Key Projects
|
||||
- **Umbraco.Cms.Api.Management** (this) - Management API controllers and models
|
||||
- **Umbraco.Cms.Api.Common** - Shared API infrastructure, base controllers
|
||||
- **Umbraco.Infrastructure** - Service implementations, data access
|
||||
- **Umbraco.Core** - Domain models, service interfaces
|
||||
|
||||
### Important Files
|
||||
- **Project file**: `src/Umbraco.Cms.Api.Management/Umbraco.Cms.Api.Management.csproj`
|
||||
- **Composer**: `src/Umbraco.Cms.Api.Management/ManagementApiComposer.cs` (DI entry point)
|
||||
- **Routing**: `src/Umbraco.Cms.Api.Management/Routing/BackOfficeAreaRoutes.cs`
|
||||
- **OpenAPI Spec**: `src/Umbraco.Cms.Api.Management/OpenApi.json` (embedded, 1.3MB)
|
||||
- **Base Controllers**: See `Umbraco.Cms.Api.Common` project
|
||||
|
||||
### Configuration
|
||||
No appsettings in this library - all configuration from parent app (Umbraco.Web.UI):
|
||||
- OpenIddict settings
|
||||
- CORS settings
|
||||
- File upload limits
|
||||
|
||||
### API Endpoints
|
||||
Base path: `/umbraco/management/api/v1/`
|
||||
|
||||
Examples:
|
||||
- `POST /umbraco/management/api/v1/document` - Create document
|
||||
- `GET /umbraco/management/api/v1/document/{id}` - Get document by key
|
||||
- `PUT /umbraco/management/api/v1/document/{id}` - Update document
|
||||
- `DELETE /umbraco/management/api/v1/document/{id}` - Delete document
|
||||
|
||||
Full spec: See OpenApi.json or Swagger UI at `/umbraco/swagger`
|
||||
|
||||
### Getting Help
|
||||
- **Root Documentation**: `/CLAUDE.md` (repository overview)
|
||||
- **API Common Docs**: `../Umbraco.Cms.Api.Common/CLAUDE.md` (shared API patterns)
|
||||
- **Core Docs**: `../Umbraco.Core/CLAUDE.md` (domain architecture)
|
||||
- **Official Docs**: https://docs.umbraco.com/umbraco-cms/reference/management-api
|
||||
@@ -5,7 +5,6 @@ using Microsoft.AspNetCore.DataProtection;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Microsoft.Net.Http.Headers;
|
||||
using Umbraco.Cms.Api.Management.Security;
|
||||
using Umbraco.Cms.Core;
|
||||
using Umbraco.Cms.Core.Configuration.Models;
|
||||
@@ -220,44 +219,10 @@ public class ConfigureBackOfficeCookieOptions : IConfigureNamedOptions<CookieAut
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
},
|
||||
// FIXME: We want to change this over to using an attribute on the backoffice controllers
|
||||
// See this for more: https://github.com/dotnet/aspnetcore/issues/63093#issuecomment-3201530217
|
||||
OnRedirectToLogin = context =>
|
||||
{
|
||||
if (IsXhr(context.Request))
|
||||
{
|
||||
context.Response.Headers.Location = context.RedirectUri;
|
||||
context.Response.StatusCode = 401;
|
||||
}
|
||||
else
|
||||
{
|
||||
context.Response.Redirect(context.RedirectUri);
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
},
|
||||
OnRedirectToAccessDenied = context =>
|
||||
{
|
||||
if (IsXhr(context.Request))
|
||||
{
|
||||
context.Response.Headers.Location = context.RedirectUri;
|
||||
context.Response.StatusCode = 403;
|
||||
}
|
||||
else
|
||||
{
|
||||
context.Response.Redirect(context.RedirectUri);
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
},
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private bool IsXhr(HttpRequest request) =>
|
||||
string.Equals(request.Query[HeaderNames.XRequestedWith], "XMLHttpRequest", StringComparison.Ordinal) ||
|
||||
string.Equals(request.Headers.XRequestedWith, "XMLHttpRequest", StringComparison.Ordinal);
|
||||
|
||||
/// <summary>
|
||||
/// Ensures the ticket is renewed if the <see cref="SecuritySettings.KeepUserLoggedIn" /> is set to true
|
||||
/// and the current request is for the get user seconds endpoint
|
||||
|
||||
+8
-7
@@ -1,8 +1,8 @@
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Microsoft.OpenApi;
|
||||
using Microsoft.OpenApi.Models;
|
||||
using Swashbuckle.AspNetCore.SwaggerGen;
|
||||
using Umbraco.Cms.Api.Common.Security;
|
||||
using Umbraco.Cms.Api.Common.OpenApi;
|
||||
using Umbraco.Cms.Api.Common.Serialization;
|
||||
using Umbraco.Cms.Api.Management.DependencyInjection;
|
||||
using Umbraco.Cms.Api.Management.OpenApi;
|
||||
@@ -11,7 +11,7 @@ namespace Umbraco.Cms.Api.Management.Configuration;
|
||||
|
||||
public class ConfigureUmbracoManagementApiSwaggerGenOptions : IConfigureOptions<SwaggerGenOptions>
|
||||
{
|
||||
private readonly IUmbracoJsonTypeInfoResolver _umbracoJsonTypeInfoResolver;
|
||||
private IUmbracoJsonTypeInfoResolver _umbracoJsonTypeInfoResolver;
|
||||
|
||||
public ConfigureUmbracoManagementApiSwaggerGenOptions(IUmbracoJsonTypeInfoResolver umbracoJsonTypeInfoResolver)
|
||||
{
|
||||
@@ -20,6 +20,7 @@ public class ConfigureUmbracoManagementApiSwaggerGenOptions : IConfigureOptions<
|
||||
|
||||
public void Configure(SwaggerGenOptions swaggerGenOptions)
|
||||
{
|
||||
|
||||
swaggerGenOptions.SwaggerDoc(
|
||||
ManagementApiConfiguration.ApiName,
|
||||
new OpenApiInfo
|
||||
@@ -50,10 +51,10 @@ public class ConfigureUmbracoManagementApiSwaggerGenOptions : IConfigureOptions<
|
||||
AuthorizationCode = new OpenApiOAuthFlow
|
||||
{
|
||||
AuthorizationUrl =
|
||||
new Uri(Paths.BackOfficeApi.AuthorizationEndpoint, UriKind.Relative),
|
||||
TokenUrl = new Uri(Paths.BackOfficeApi.TokenEndpoint, UriKind.Relative),
|
||||
},
|
||||
},
|
||||
new Uri(Common.Security.Paths.BackOfficeApi.AuthorizationEndpoint, UriKind.Relative),
|
||||
TokenUrl = new Uri(Common.Security.Paths.BackOfficeApi.TokenEndpoint, UriKind.Relative)
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Sets Security requirement on backoffice apis
|
||||
|
||||
@@ -29,6 +29,39 @@ public abstract class ContentCollectionControllerBase<TContent, TCollectionRespo
|
||||
_flagProviders = flagProvider;
|
||||
}
|
||||
|
||||
[Obsolete("Use the constructer with all parameters. To be removed in Umbraco 18")]
|
||||
protected ContentCollectionControllerBase(IUmbracoMapper mapper)
|
||||
: this(mapper, StaticServiceProvider.Instance.GetRequiredService<FlagProviderCollection>())
|
||||
{
|
||||
}
|
||||
|
||||
[Obsolete("This method is no longer used and will be removed in Umbraco 17.")]
|
||||
protected IActionResult CollectionResult(ListViewPagedModel<TContent> result)
|
||||
{
|
||||
PagedModel<TContent> collectionItemsResult = result.Items;
|
||||
ListViewConfiguration collectionConfiguration = result.ListViewConfiguration;
|
||||
|
||||
var collectionPropertyAliases = collectionConfiguration
|
||||
.IncludeProperties
|
||||
.Select(p => p.Alias)
|
||||
.WhereNotNull()
|
||||
.ToArray();
|
||||
|
||||
List<TCollectionResponseModel> collectionResponseModels =
|
||||
_mapper.MapEnumerable<TContent, TCollectionResponseModel>(collectionItemsResult.Items, context =>
|
||||
{
|
||||
context.SetIncludedProperties(collectionPropertyAliases);
|
||||
});
|
||||
|
||||
var pageViewModel = new PagedViewModel<TCollectionResponseModel>
|
||||
{
|
||||
Items = collectionResponseModels,
|
||||
Total = collectionItemsResult.Total,
|
||||
};
|
||||
|
||||
return Ok(pageViewModel);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a collection result from the provided collection response models and total number of items.
|
||||
/// </summary>
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Umbraco.Cms.Api.Management.ViewModels.Content;
|
||||
using Umbraco.Cms.Core.Mapping;
|
||||
using Umbraco.Cms.Core.Models.ContentEditing;
|
||||
using Umbraco.Cms.Core.Models.ContentEditing.Validation;
|
||||
using Umbraco.Cms.Core.PropertyEditors.Validation;
|
||||
@@ -10,6 +12,7 @@ namespace Umbraco.Cms.Api.Management.Controllers.Content;
|
||||
|
||||
public abstract class ContentControllerBase : ManagementApiControllerBase
|
||||
{
|
||||
|
||||
protected IActionResult ContentEditingOperationStatusResult(ContentEditingOperationStatus status)
|
||||
=> OperationStatusResult(status, problemDetailsBuilder => status switch
|
||||
{
|
||||
@@ -95,17 +98,6 @@ public abstract class ContentControllerBase : ManagementApiControllerBase
|
||||
.Build()),
|
||||
});
|
||||
|
||||
protected IActionResult GetReferencesOperationStatusResult(GetReferencesOperationStatus status)
|
||||
=> OperationStatusResult(status, problemDetailsBuilder => status switch
|
||||
{
|
||||
GetReferencesOperationStatus.ContentNotFound => NotFound(problemDetailsBuilder
|
||||
.WithTitle("The requested content could not be found")
|
||||
.Build()),
|
||||
_ => StatusCode(StatusCodes.Status500InternalServerError, problemDetailsBuilder
|
||||
.WithTitle("Unknown get references operation status.")
|
||||
.Build()),
|
||||
});
|
||||
|
||||
protected IActionResult ContentEditingOperationStatusResult<TContentModelBase, TValueModel, TVariantModel>(
|
||||
ContentEditingOperationStatus status,
|
||||
TContentModelBase requestModel,
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
using Asp.Versioning;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Umbraco.Cms.Api.Management.Factories;
|
||||
using Umbraco.Cms.Api.Management.ViewModels.DataType;
|
||||
using Umbraco.Cms.Core;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
using Umbraco.Cms.Core.Services.OperationStatus;
|
||||
|
||||
namespace Umbraco.Cms.Api.Management.Controllers.DataType;
|
||||
|
||||
[ApiVersion("1.0")]
|
||||
[Obsolete("Please use ReferencedByDataTypeController and the referenced-by endpoint. Scheduled for removal in Umbraco 17.")]
|
||||
public class ReferencesDataTypeController : DataTypeControllerBase
|
||||
{
|
||||
private readonly IDataTypeService _dataTypeService;
|
||||
private readonly IDataTypeReferencePresentationFactory _dataTypeReferencePresentationFactory;
|
||||
|
||||
public ReferencesDataTypeController(IDataTypeService dataTypeService, IDataTypeReferencePresentationFactory dataTypeReferencePresentationFactory)
|
||||
{
|
||||
_dataTypeService = dataTypeService;
|
||||
_dataTypeReferencePresentationFactory = dataTypeReferencePresentationFactory;
|
||||
}
|
||||
|
||||
[HttpGet("{id:guid}/references")]
|
||||
[MapToApiVersion("1.0")]
|
||||
[ProducesResponseType(typeof(DataTypeReferenceResponseModel[]), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
|
||||
public async Task<IActionResult> References(CancellationToken cancellationToken, Guid id)
|
||||
{
|
||||
Attempt<IReadOnlyDictionary<Udi, IEnumerable<string>>, DataTypeOperationStatus> result = await _dataTypeService.GetReferencesAsync(id);
|
||||
if (result.Success == false)
|
||||
{
|
||||
return DataTypeOperationStatusResult(result.Status);
|
||||
}
|
||||
|
||||
DataTypeReferenceResponseModel[] viewModels = _dataTypeReferencePresentationFactory.CreateDataTypeReferenceViewModels(result.Result).ToArray();
|
||||
return Ok(viewModels);
|
||||
}
|
||||
}
|
||||
@@ -2,10 +2,13 @@ using Asp.Versioning;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Umbraco.Cms.Api.Management.Factories;
|
||||
using Umbraco.Cms.Api.Management.ViewModels.Document;
|
||||
using Umbraco.Cms.Core.Actions;
|
||||
using Umbraco.Cms.Core.DependencyInjection;
|
||||
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,6 +22,31 @@ public class ByKeyDocumentController : DocumentControllerBase
|
||||
private readonly IDocumentPresentationFactory _documentPresentationFactory;
|
||||
private readonly IContentQueryService _contentQueryService;
|
||||
|
||||
[Obsolete("Scheduled for removal in v17")]
|
||||
public ByKeyDocumentController(
|
||||
IAuthorizationService authorizationService,
|
||||
IContentEditingService contentEditingService,
|
||||
IDocumentPresentationFactory documentPresentationFactory)
|
||||
{
|
||||
_authorizationService = authorizationService;
|
||||
_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,
|
||||
|
||||
+6
@@ -23,6 +23,12 @@ public abstract class DocumentCollectionControllerBase : ContentCollectionContro
|
||||
{
|
||||
}
|
||||
|
||||
[Obsolete("Please use the constructor with all parameters. Scheduled to be removed in V18")]
|
||||
protected DocumentCollectionControllerBase(IUmbracoMapper mapper)
|
||||
: base(mapper)
|
||||
{
|
||||
}
|
||||
|
||||
protected IActionResult CollectionOperationStatusResult(ContentCollectionOperationStatus status)
|
||||
=> ContentCollectionOperationStatusResult(status, "document");
|
||||
}
|
||||
|
||||
@@ -1,53 +0,0 @@
|
||||
using Asp.Versioning;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Umbraco.Cms.Api.Common.Builders;
|
||||
using Umbraco.Cms.Api.Management.Factories;
|
||||
using Umbraco.Cms.Api.Management.ViewModels.Document;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
|
||||
namespace Umbraco.Cms.Api.Management.Controllers.Document;
|
||||
|
||||
[ApiVersion("1.0")]
|
||||
public class DocumentPreviewUrlController : DocumentControllerBase
|
||||
{
|
||||
private readonly IContentService _contentService;
|
||||
private readonly IDocumentUrlFactory _documentUrlFactory;
|
||||
|
||||
public DocumentPreviewUrlController(
|
||||
IContentService contentService,
|
||||
IDocumentUrlFactory documentUrlFactory)
|
||||
{
|
||||
_contentService = contentService;
|
||||
_documentUrlFactory = documentUrlFactory;
|
||||
}
|
||||
|
||||
[MapToApiVersion("1.0")]
|
||||
[HttpGet("{id:guid}/preview-url")]
|
||||
[ProducesResponseType(typeof(DocumentUrlInfo), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
|
||||
public async Task<IActionResult> GetPreviewUrl(Guid id, string providerAlias, string? culture, string? segment)
|
||||
{
|
||||
IContent? content = _contentService.GetById(id);
|
||||
if (content is null)
|
||||
{
|
||||
return NotFound(new ProblemDetailsBuilder()
|
||||
.WithTitle("Document not found")
|
||||
.WithDetail("The requested document did not exist.")
|
||||
.Build());
|
||||
}
|
||||
|
||||
DocumentUrlInfo? previewUrlInfo = await _documentUrlFactory.GetPreviewUrlAsync(content, providerAlias, culture, segment);
|
||||
if (previewUrlInfo is null)
|
||||
{
|
||||
return BadRequest(new ProblemDetailsBuilder()
|
||||
.WithTitle("No preview URL for document")
|
||||
.WithDetail("Failed to produce a preview URL for the requested document.")
|
||||
.Build());
|
||||
}
|
||||
|
||||
return Ok(previewUrlInfo);
|
||||
}
|
||||
}
|
||||
@@ -1,31 +1,60 @@
|
||||
using Asp.Versioning;
|
||||
using Asp.Versioning;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Umbraco.Cms.Api.Management.ViewModels.Document;
|
||||
using Umbraco.Cms.Core.Actions;
|
||||
using Umbraco.Cms.Core.DependencyInjection;
|
||||
using Umbraco.Cms.Core.Mapping;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Security.Authorization;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
using Umbraco.Cms.Web.Common.Authorization;
|
||||
using Umbraco.Extensions;
|
||||
|
||||
namespace Umbraco.Cms.Api.Management.Controllers.Document;
|
||||
|
||||
[ApiVersion("1.0")]
|
||||
public class DomainsController : DocumentControllerBase
|
||||
{
|
||||
private readonly IAuthorizationService _authorizationService;
|
||||
private readonly IDomainService _domainService;
|
||||
private readonly IUmbracoMapper _umbracoMapper;
|
||||
|
||||
public DomainsController(IDomainService domainService, IUmbracoMapper umbracoMapper)
|
||||
[ActivatorUtilitiesConstructor]
|
||||
public DomainsController(IAuthorizationService authorizationService, IDomainService domainService, IUmbracoMapper umbracoMapper)
|
||||
{
|
||||
_authorizationService = authorizationService;
|
||||
_domainService = domainService;
|
||||
_umbracoMapper = umbracoMapper;
|
||||
}
|
||||
|
||||
[Obsolete("Please use the constructor taking all parameters. Scheduled for removal in Umbraco 18.")]
|
||||
public DomainsController(IDomainService domainService, IUmbracoMapper umbracoMapper)
|
||||
: this(
|
||||
StaticServiceProvider.Instance.GetRequiredService<IAuthorizationService>(),
|
||||
domainService,
|
||||
umbracoMapper)
|
||||
{
|
||||
}
|
||||
|
||||
[MapToApiVersion("1.0")]
|
||||
[HttpGet("{id:guid}/domains")]
|
||||
[ProducesResponseType(typeof(DomainsResponseModel), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
|
||||
public async Task<IActionResult> Domains(CancellationToken cancellationToken, Guid id)
|
||||
{
|
||||
AuthorizationResult authorizationResult = await _authorizationService.AuthorizeResourceAsync(
|
||||
User,
|
||||
ContentPermissionResource.WithKeys(ActionBrowse.ActionLetter, id),
|
||||
AuthorizationPolicies.ContentPermissionByResource);
|
||||
|
||||
if (!authorizationResult.Succeeded)
|
||||
{
|
||||
return Forbidden();
|
||||
}
|
||||
|
||||
IDomain[] assignedDomains = (await _domainService.GetAssignedDomainsAsync(id, true))
|
||||
.OrderBy(d => d.SortOrder)
|
||||
.ToArray();
|
||||
|
||||
+11
-40
@@ -4,10 +4,8 @@ using Microsoft.AspNetCore.Mvc;
|
||||
using Umbraco.Cms.Api.Common.ViewModels.Pagination;
|
||||
using Umbraco.Cms.Api.Management.Factories;
|
||||
using Umbraco.Cms.Api.Management.ViewModels.TrackedReferences;
|
||||
using Umbraco.Cms.Core;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
using Umbraco.Cms.Core.Services.OperationStatus;
|
||||
|
||||
namespace Umbraco.Cms.Api.Management.Controllers.Document.References;
|
||||
|
||||
@@ -17,16 +15,22 @@ public class ReferencedByDocumentController : DocumentControllerBase
|
||||
private readonly ITrackedReferencesService _trackedReferencesService;
|
||||
private readonly IRelationTypePresentationFactory _relationTypePresentationFactory;
|
||||
|
||||
public ReferencedByDocumentController(
|
||||
ITrackedReferencesService trackedReferencesService,
|
||||
IRelationTypePresentationFactory relationTypePresentationFactory)
|
||||
public ReferencedByDocumentController(ITrackedReferencesService trackedReferencesService, IRelationTypePresentationFactory relationTypePresentationFactory)
|
||||
{
|
||||
_trackedReferencesService = trackedReferencesService;
|
||||
_relationTypePresentationFactory = relationTypePresentationFactory;
|
||||
}
|
||||
|
||||
[Obsolete("Use the ReferencedBy2 action method instead. Scheduled for removal in Umbraco 19, when ReferencedBy2 will be renamed back to ReferencedBy.")]
|
||||
[NonAction]
|
||||
/// <summary>
|
||||
/// Gets a paged 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.
|
||||
/// This is basically finding parents of relations.
|
||||
/// </remarks>
|
||||
[HttpGet("{id:guid}/referenced-by")]
|
||||
[MapToApiVersion("1.0")]
|
||||
[ProducesResponseType(typeof(PagedViewModel<IReferenceResponseModel>), StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<PagedViewModel<IReferenceResponseModel>>> ReferencedBy(
|
||||
CancellationToken cancellationToken,
|
||||
Guid id,
|
||||
@@ -43,37 +47,4 @@ public class ReferencedByDocumentController : DocumentControllerBase
|
||||
|
||||
return pagedViewModel;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a paged 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.
|
||||
/// This is basically finding parents of relations.
|
||||
/// </remarks>
|
||||
[HttpGet("{id:guid}/referenced-by")]
|
||||
[MapToApiVersion("1.0")]
|
||||
[ProducesResponseType(typeof(PagedViewModel<IReferenceResponseModel>), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
|
||||
public async Task<IActionResult> ReferencedBy2(
|
||||
CancellationToken cancellationToken,
|
||||
Guid id,
|
||||
int skip = 0,
|
||||
int take = 20)
|
||||
{
|
||||
Attempt<PagedModel<RelationItemModel>, GetReferencesOperationStatus> relationItemsAttempt = await _trackedReferencesService.GetPagedRelationsForItemAsync(id, UmbracoObjectTypes.Document, skip, take, true);
|
||||
|
||||
if (relationItemsAttempt.Success is false)
|
||||
{
|
||||
return GetReferencesOperationStatusResult(relationItemsAttempt.Status);
|
||||
}
|
||||
|
||||
var pagedViewModel = new PagedViewModel<IReferenceResponseModel>
|
||||
{
|
||||
Total = relationItemsAttempt.Result.Total,
|
||||
Items = await _relationTypePresentationFactory.CreateReferenceResponseModelsAsync(relationItemsAttempt.Result.Items),
|
||||
};
|
||||
|
||||
return Ok(pagedViewModel);
|
||||
}
|
||||
}
|
||||
|
||||
+6
-35
@@ -3,11 +3,9 @@ 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;
|
||||
using Umbraco.Cms.Core.Services.OperationStatus;
|
||||
|
||||
namespace Umbraco.Cms.Api.Management.Controllers.Document.References;
|
||||
|
||||
@@ -17,32 +15,12 @@ public class ReferencedDescendantsDocumentController : DocumentControllerBase
|
||||
private readonly ITrackedReferencesService _trackedReferencesSkipTakeService;
|
||||
private readonly IUmbracoMapper _umbracoMapper;
|
||||
|
||||
public ReferencedDescendantsDocumentController(
|
||||
ITrackedReferencesService trackedReferencesSkipTakeService,
|
||||
IUmbracoMapper umbracoMapper)
|
||||
public ReferencedDescendantsDocumentController(ITrackedReferencesService trackedReferencesSkipTakeService, IUmbracoMapper umbracoMapper)
|
||||
{
|
||||
_trackedReferencesSkipTakeService = trackedReferencesSkipTakeService;
|
||||
_umbracoMapper = umbracoMapper;
|
||||
}
|
||||
|
||||
[Obsolete("Use the ReferencedDescendants2 action method instead. Scheduled for removal in Umbraco 19, when ReferencedDescendants2 will be renamed back to ReferencedDescendants.")]
|
||||
[NonAction]
|
||||
public async Task<ActionResult<PagedViewModel<ReferenceByIdModel>>> ReferencedDescendants(
|
||||
CancellationToken cancellationToken,
|
||||
Guid id,
|
||||
int skip = 0,
|
||||
int take = 20)
|
||||
{
|
||||
PagedModel<RelationItemModel> relationItems = await _trackedReferencesSkipTakeService.GetPagedDescendantsInReferencesAsync(id, skip, take, true);
|
||||
var pagedViewModel = new PagedViewModel<ReferenceByIdModel>
|
||||
{
|
||||
Total = relationItems.Total,
|
||||
Items = _umbracoMapper.MapEnumerable<RelationItemModel, ReferenceByIdModel>(relationItems.Items),
|
||||
};
|
||||
|
||||
return pagedViewModel;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a paged list of the descendant nodes of the current item used in any kind of relation.
|
||||
/// </summary>
|
||||
@@ -54,26 +32,19 @@ public class ReferencedDescendantsDocumentController : DocumentControllerBase
|
||||
[HttpGet("{id:guid}/referenced-descendants")]
|
||||
[MapToApiVersion("1.0")]
|
||||
[ProducesResponseType(typeof(PagedViewModel<ReferenceByIdModel>), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
|
||||
public async Task<IActionResult> ReferencedDescendants2(
|
||||
public async Task<ActionResult<PagedViewModel<ReferenceByIdModel>>> ReferencedDescendants(
|
||||
CancellationToken cancellationToken,
|
||||
Guid id,
|
||||
int skip = 0,
|
||||
int take = 20)
|
||||
{
|
||||
Attempt<PagedModel<RelationItemModel>, GetReferencesOperationStatus> relationItemsAttempt = await _trackedReferencesSkipTakeService.GetPagedDescendantsInReferencesAsync(id, UmbracoObjectTypes.Document, skip, take, true);
|
||||
|
||||
if (relationItemsAttempt.Success is false)
|
||||
{
|
||||
return GetReferencesOperationStatusResult(relationItemsAttempt.Status);
|
||||
}
|
||||
|
||||
PagedModel<RelationItemModel> relationItems = await _trackedReferencesSkipTakeService.GetPagedDescendantsInReferencesAsync(id, skip, take, true);
|
||||
var pagedViewModel = new PagedViewModel<ReferenceByIdModel>
|
||||
{
|
||||
Total = relationItemsAttempt.Result.Total,
|
||||
Items = _umbracoMapper.MapEnumerable<RelationItemModel, ReferenceByIdModel>(relationItemsAttempt.Result.Items),
|
||||
Total = relationItems.Total,
|
||||
Items = _umbracoMapper.MapEnumerable<RelationItemModel, ReferenceByIdModel>(relationItems.Items),
|
||||
};
|
||||
|
||||
return Ok(pagedViewModel);
|
||||
return pagedViewModel;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,19 @@
|
||||
using Asp.Versioning;
|
||||
using Asp.Versioning;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Umbraco.Cms.Api.Management.Factories;
|
||||
using Umbraco.Cms.Api.Management.ViewModels.Document;
|
||||
using Umbraco.Cms.Core;
|
||||
using Umbraco.Cms.Core.Actions;
|
||||
using Umbraco.Cms.Core.DependencyInjection;
|
||||
using Umbraco.Cms.Core.Mapping;
|
||||
using Umbraco.Cms.Core.Models.ContentEditing;
|
||||
using Umbraco.Cms.Core.Security.Authorization;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
using Umbraco.Cms.Core.Services.OperationStatus;
|
||||
using Umbraco.Cms.Web.Common.Authorization;
|
||||
using Umbraco.Extensions;
|
||||
|
||||
namespace Umbraco.Cms.Api.Management.Controllers.Document;
|
||||
@@ -15,17 +21,30 @@ namespace Umbraco.Cms.Api.Management.Controllers.Document;
|
||||
[ApiVersion("1.0")]
|
||||
public class UpdateDomainsController : DocumentControllerBase
|
||||
{
|
||||
private readonly IAuthorizationService _authorizationService;
|
||||
private readonly IDomainService _domainService;
|
||||
private readonly IUmbracoMapper _umbracoMapper;
|
||||
private readonly IDomainPresentationFactory _domainPresentationFactory;
|
||||
|
||||
public UpdateDomainsController(IDomainService domainService, IUmbracoMapper umbracoMapper, IDomainPresentationFactory domainPresentationFactory)
|
||||
[ActivatorUtilitiesConstructor]
|
||||
public UpdateDomainsController(IAuthorizationService authorizationService, IDomainService domainService, IUmbracoMapper umbracoMapper, IDomainPresentationFactory domainPresentationFactory)
|
||||
{
|
||||
_authorizationService = authorizationService;
|
||||
_domainService = domainService;
|
||||
_umbracoMapper = umbracoMapper;
|
||||
_domainPresentationFactory = domainPresentationFactory;
|
||||
}
|
||||
|
||||
[Obsolete("Please use the constructor taking all parameters. Scheduled for removal in Umbraco 18.")]
|
||||
public UpdateDomainsController(IDomainService domainService, IUmbracoMapper umbracoMapper, IDomainPresentationFactory domainPresentationFactory)
|
||||
: this(
|
||||
StaticServiceProvider.Instance.GetRequiredService<IAuthorizationService>(),
|
||||
domainService,
|
||||
umbracoMapper,
|
||||
domainPresentationFactory)
|
||||
{
|
||||
}
|
||||
|
||||
[MapToApiVersion("1.0")]
|
||||
[HttpPut("{id:guid}/domains")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
@@ -37,6 +56,16 @@ public class UpdateDomainsController : DocumentControllerBase
|
||||
Guid id,
|
||||
UpdateDomainsRequestModel updateModel)
|
||||
{
|
||||
AuthorizationResult authorizationResult = await _authorizationService.AuthorizeResourceAsync(
|
||||
User,
|
||||
ContentPermissionResource.WithKeys(ActionAssignDomain.ActionLetter, id),
|
||||
AuthorizationPolicies.ContentPermissionByResource);
|
||||
|
||||
if (!authorizationResult.Succeeded)
|
||||
{
|
||||
return Forbidden();
|
||||
}
|
||||
|
||||
DomainsUpdateModel domainsUpdateModel = _umbracoMapper.Map<DomainsUpdateModel>(updateModel)!;
|
||||
|
||||
Attempt<DomainUpdateResult, DomainOperationStatus> result = await _domainService.UpdateDomainsAsync(id, domainsUpdateModel);
|
||||
|
||||
+31
-1
@@ -1,33 +1,63 @@
|
||||
using Asp.Versioning;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Umbraco.Cms.Api.Management.ViewModels.Document;
|
||||
using Umbraco.Cms.Core.Actions;
|
||||
using Umbraco.Cms.Core.DependencyInjection;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Security;
|
||||
using Umbraco.Cms.Core.Security.Authorization;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
using Umbraco.Cms.Web.Common.Authorization;
|
||||
using Umbraco.Extensions;
|
||||
|
||||
namespace Umbraco.Cms.Api.Management.Controllers.Document;
|
||||
|
||||
[ApiVersion("1.0")]
|
||||
public class UpdateNotificationsController : DocumentControllerBase
|
||||
{
|
||||
private readonly IAuthorizationService _authorizationService;
|
||||
private readonly IContentEditingService _contentEditingService;
|
||||
private readonly INotificationService _notificationService;
|
||||
private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor;
|
||||
|
||||
public UpdateNotificationsController(IContentEditingService contentEditingService, INotificationService notificationService, IBackOfficeSecurityAccessor backOfficeSecurityAccessor)
|
||||
[ActivatorUtilitiesConstructor]
|
||||
public UpdateNotificationsController(IAuthorizationService authorizationService, IContentEditingService contentEditingService, INotificationService notificationService, IBackOfficeSecurityAccessor backOfficeSecurityAccessor)
|
||||
{
|
||||
_authorizationService = authorizationService;
|
||||
_contentEditingService = contentEditingService;
|
||||
_notificationService = notificationService;
|
||||
_backOfficeSecurityAccessor = backOfficeSecurityAccessor;
|
||||
}
|
||||
|
||||
[Obsolete("Please use the constructor taking all parameters. Scheduled for removal in Umbraco 18.")]
|
||||
public UpdateNotificationsController(IContentEditingService contentEditingService, INotificationService notificationService, IBackOfficeSecurityAccessor backOfficeSecurityAccessor)
|
||||
: this(
|
||||
StaticServiceProvider.Instance.GetRequiredService<IAuthorizationService>(),
|
||||
contentEditingService,
|
||||
notificationService,
|
||||
backOfficeSecurityAccessor)
|
||||
{
|
||||
}
|
||||
|
||||
[MapToApiVersion("1.0")]
|
||||
[HttpPut("{id:guid}/notifications")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
|
||||
public async Task<IActionResult> UpdateNotifications(CancellationToken cancellationToken, Guid id, UpdateDocumentNotificationsRequestModel updateModel)
|
||||
{
|
||||
AuthorizationResult authorizationResult = await _authorizationService.AuthorizeResourceAsync(
|
||||
User,
|
||||
ContentPermissionResource.WithKeys(ActionBrowse.ActionLetter, id),
|
||||
AuthorizationPolicies.ContentPermissionByResource);
|
||||
|
||||
if (!authorizationResult.Succeeded)
|
||||
{
|
||||
return Forbidden();
|
||||
}
|
||||
|
||||
IContent? content = await _contentEditingService.GetAsync(id);
|
||||
if (content == null)
|
||||
{
|
||||
|
||||
+14
@@ -21,6 +21,20 @@ public class ValidateCreateDocumentController : CreateDocumentControllerBase
|
||||
private readonly IContentEditingService _contentEditingService;
|
||||
private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor;
|
||||
|
||||
[Obsolete("Please use the constructor taking all parameters. Scheduled for removal in Umbraco 17.")]
|
||||
public ValidateCreateDocumentController(
|
||||
IAuthorizationService authorizationService,
|
||||
IDocumentEditingPresentationFactory documentEditingPresentationFactory,
|
||||
IContentEditingService contentEditingService)
|
||||
: this(
|
||||
authorizationService,
|
||||
documentEditingPresentationFactory,
|
||||
contentEditingService,
|
||||
StaticServiceProvider.Instance.GetRequiredService<IBackOfficeSecurityAccessor>())
|
||||
{
|
||||
}
|
||||
|
||||
[ActivatorUtilitiesConstructor]
|
||||
public ValidateCreateDocumentController(
|
||||
IAuthorizationService authorizationService,
|
||||
IDocumentEditingPresentationFactory documentEditingPresentationFactory,
|
||||
|
||||
+14
@@ -21,6 +21,20 @@ public class ValidateUpdateDocumentController : UpdateDocumentControllerBase
|
||||
private readonly IDocumentEditingPresentationFactory _documentEditingPresentationFactory;
|
||||
private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor;
|
||||
|
||||
[Obsolete("Please use the constructor taking all parameters. Scheduled for removal in Umbraco 17.")]
|
||||
public ValidateUpdateDocumentController(
|
||||
IAuthorizationService authorizationService,
|
||||
IContentEditingService contentEditingService,
|
||||
IDocumentEditingPresentationFactory documentEditingPresentationFactory)
|
||||
: this(
|
||||
authorizationService,
|
||||
contentEditingService,
|
||||
documentEditingPresentationFactory,
|
||||
StaticServiceProvider.Instance.GetRequiredService<IBackOfficeSecurityAccessor>())
|
||||
{
|
||||
}
|
||||
|
||||
[ActivatorUtilitiesConstructor]
|
||||
public ValidateUpdateDocumentController(
|
||||
IAuthorizationService authorizationService,
|
||||
IContentEditingService contentEditingService,
|
||||
|
||||
-47
@@ -1,47 +0,0 @@
|
||||
using Asp.Versioning;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Umbraco.Cms.Api.Management.Controllers.Template;
|
||||
using Umbraco.Cms.Api.Management.ViewModels.DocumentType;
|
||||
using Umbraco.Cms.Core;
|
||||
using Umbraco.Cms.Core.Security;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
using Umbraco.Cms.Core.Services.OperationStatus;
|
||||
|
||||
namespace Umbraco.Cms.Api.Management.Controllers.DocumentType;
|
||||
|
||||
[ApiVersion("1.0")]
|
||||
public class CreateDocumentTypeTemplateController : DocumentTypeControllerBase
|
||||
{
|
||||
private readonly IContentTypeService _contentTypeService;
|
||||
private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor;
|
||||
|
||||
public CreateDocumentTypeTemplateController(
|
||||
IContentTypeService contentTypeService,
|
||||
IBackOfficeSecurityAccessor backOfficeSecurityAccessor)
|
||||
{
|
||||
_contentTypeService = contentTypeService;
|
||||
_backOfficeSecurityAccessor = backOfficeSecurityAccessor;
|
||||
}
|
||||
|
||||
[HttpPost("{id:guid}/template")]
|
||||
[MapToApiVersion("1.0")]
|
||||
[ProducesResponseType(StatusCodes.Status201Created)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)]
|
||||
public async Task<IActionResult> CreateTemplate(
|
||||
CancellationToken cancellationToken,
|
||||
Guid id,
|
||||
CreateDocumentTypeTemplateRequestModel requestModel)
|
||||
{
|
||||
Attempt<Guid?, ContentTypeOperationStatus> result = await _contentTypeService.CreateTemplateAsync(
|
||||
id,
|
||||
requestModel.Name,
|
||||
requestModel.Alias,
|
||||
requestModel.IsDefault,
|
||||
CurrentUserKey(_backOfficeSecurityAccessor));
|
||||
|
||||
return result.Success
|
||||
? CreatedAtId<ByKeyTemplateController>(controller => nameof(controller.ByKey), result.Result!.Value)
|
||||
: OperationStatusResult(result.Status);
|
||||
}
|
||||
}
|
||||
@@ -41,10 +41,6 @@ public abstract class DocumentTypeControllerBase : ManagementApiControllerBase
|
||||
.WithTitle("Invalid property type alias")
|
||||
.WithDetail("One or more property type aliases are invalid")
|
||||
.Build()),
|
||||
ContentTypeOperationStatus.InvalidTemplateAlias => new BadRequestObjectResult(problemDetailsBuilder
|
||||
.WithTitle("Invalid template alias")
|
||||
.WithDetail("The specified template alias is invalid")
|
||||
.Build()),
|
||||
ContentTypeOperationStatus.PropertyTypeAliasCannotEqualContentTypeAlias => new BadRequestObjectResult(problemDetailsBuilder
|
||||
.WithTitle("Invalid property type alias")
|
||||
.WithDetail("The property type alias cannot be the same as the content type alias")
|
||||
|
||||
+23
@@ -16,12 +16,35 @@ public class SearchDocumentTypeItemController : DocumentTypeItemControllerBase
|
||||
private readonly IUmbracoMapper _mapper;
|
||||
private readonly IContentTypeSearchService _contentTypeSearchService;
|
||||
|
||||
[Obsolete("Please use ctor that only accepts IUmbracoMapper & IContentTypeSearchService, scheduled for removal in v17")]
|
||||
public SearchDocumentTypeItemController(IEntitySearchService entitySearchService, IContentTypeService contentTypeService, IUmbracoMapper mapper)
|
||||
: this(mapper, StaticServiceProvider.Instance.GetRequiredService<IContentTypeSearchService>())
|
||||
{
|
||||
}
|
||||
|
||||
[Obsolete("Please use ctor that only accepts IUmbracoMapper & IContentTypeSearchService, scheduled for removal in v17")]
|
||||
// We need to have this constructor, or else we get ambiguous constructor error
|
||||
public SearchDocumentTypeItemController(
|
||||
IEntitySearchService entitySearchService,
|
||||
IContentTypeService contentTypeService,
|
||||
IUmbracoMapper mapper,
|
||||
IContentTypeSearchService contentTypeSearchService)
|
||||
: this(mapper, contentTypeSearchService)
|
||||
{
|
||||
}
|
||||
|
||||
[ActivatorUtilitiesConstructor]
|
||||
public SearchDocumentTypeItemController(IUmbracoMapper mapper, IContentTypeSearchService contentTypeSearchService)
|
||||
{
|
||||
_mapper = mapper;
|
||||
_contentTypeSearchService = contentTypeSearchService;
|
||||
}
|
||||
|
||||
[NonAction]
|
||||
[Obsolete("Scheduled to be removed in v16, use the non obsoleted method instead")]
|
||||
public async Task<IActionResult> Search(CancellationToken cancellationToken, string query, int skip = 0, int take = 100)
|
||||
=> await SearchDocumentType(cancellationToken, query, null, skip, take);
|
||||
|
||||
[HttpGet("search")]
|
||||
[MapToApiVersion("1.0")]
|
||||
[ProducesResponseType(typeof(PagedModel<DocumentTypeItemResponseModel>), StatusCodes.Status200OK)]
|
||||
|
||||
@@ -11,13 +11,12 @@ using Umbraco.Cms.Api.Common.ViewModels.Pagination;
|
||||
|
||||
namespace Umbraco.Cms.Api.Management.Controllers.Help;
|
||||
|
||||
[Obsolete("This is no longer used and will be removed in v19")]
|
||||
[ApiVersion("1.0")]
|
||||
public class GetHelpController : HelpControllerBase
|
||||
{
|
||||
private readonly ILogger<GetHelpController> _logger;
|
||||
private readonly IJsonSerializer _jsonSerializer;
|
||||
private readonly HelpPageSettings _helpPageSettings;
|
||||
private HelpPageSettings _helpPageSettings;
|
||||
|
||||
public GetHelpController(
|
||||
IOptionsMonitor<HelpPageSettings> helpPageSettings,
|
||||
@@ -27,8 +26,11 @@ public class GetHelpController : HelpControllerBase
|
||||
_logger = logger;
|
||||
_jsonSerializer = jsonSerializer;
|
||||
_helpPageSettings = helpPageSettings.CurrentValue;
|
||||
helpPageSettings.OnChange(UpdateHelpPageSettings);
|
||||
}
|
||||
|
||||
private void UpdateHelpPageSettings(HelpPageSettings settings) => _helpPageSettings = settings;
|
||||
|
||||
[HttpGet]
|
||||
[MapToApiVersion("1.0")]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)]
|
||||
|
||||
@@ -3,7 +3,6 @@ using Umbraco.Cms.Api.Management.Routing;
|
||||
|
||||
namespace Umbraco.Cms.Api.Management.Controllers.Help;
|
||||
|
||||
[Obsolete("This is no longer used and will be removed in v19")]
|
||||
[VersionedApiBackOfficeRoute("help")]
|
||||
[ApiExplorerSettings(GroupName = "Help")]
|
||||
public abstract class HelpControllerBase : ManagementApiControllerBase
|
||||
|
||||
@@ -26,9 +26,9 @@ public class RebuildIndexerController : IndexerControllerBase
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Rebuilds the index.
|
||||
/// Rebuilds the index
|
||||
/// </summary>
|
||||
/// <param name="indexName">The name of the index to rebuild.</param>
|
||||
/// <param name="indexName"></param>
|
||||
/// <returns></returns>
|
||||
[HttpPost("{indexName}/rebuild")]
|
||||
[MapToApiVersion("1.0")]
|
||||
@@ -36,7 +36,7 @@ public class RebuildIndexerController : IndexerControllerBase
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status409Conflict)]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
public async Task<IActionResult> Rebuild(CancellationToken cancellationToken, string indexName)
|
||||
public Task<IActionResult> Rebuild(CancellationToken cancellationToken, string indexName)
|
||||
{
|
||||
if (!_examineManager.TryGetIndex(indexName, out IIndex? index))
|
||||
{
|
||||
@@ -48,7 +48,7 @@ public class RebuildIndexerController : IndexerControllerBase
|
||||
Type = "Error",
|
||||
};
|
||||
|
||||
return NotFound(invalidModelProblem);
|
||||
return Task.FromResult<IActionResult>(NotFound(invalidModelProblem));
|
||||
}
|
||||
|
||||
if (!_indexingRebuilderService.CanRebuild(index.Name))
|
||||
@@ -57,19 +57,19 @@ public class RebuildIndexerController : IndexerControllerBase
|
||||
{
|
||||
Title = "Could not validate the populator",
|
||||
Detail =
|
||||
$"The index {index.Name} could not be rebuilt because we could not validate its associated {typeof(IIndexPopulator)}",
|
||||
$"The index {index?.Name} could not be rebuilt because we could not validate its associated {typeof(IIndexPopulator)}",
|
||||
Status = StatusCodes.Status400BadRequest,
|
||||
Type = "Error",
|
||||
};
|
||||
|
||||
return BadRequest(invalidModelProblem);
|
||||
return Task.FromResult<IActionResult>(BadRequest(invalidModelProblem));
|
||||
}
|
||||
|
||||
_logger.LogInformation("Rebuilding index '{IndexName}'", indexName);
|
||||
|
||||
if (await _indexingRebuilderService.TryRebuildAsync(index, indexName))
|
||||
if (_indexingRebuilderService.TryRebuild(index, indexName))
|
||||
{
|
||||
return Ok();
|
||||
return Task.FromResult<IActionResult>(Ok());
|
||||
}
|
||||
|
||||
var problemDetails = new ProblemDetails
|
||||
@@ -80,6 +80,6 @@ public class RebuildIndexerController : IndexerControllerBase
|
||||
Type = "Error",
|
||||
};
|
||||
|
||||
return Conflict(problemDetails);
|
||||
return Task.FromResult<IActionResult>(Conflict(problemDetails));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
using Asp.Versioning;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Umbraco.Cms.Core.Mapping;
|
||||
using Umbraco.Cms.Infrastructure.Install;
|
||||
using Umbraco.Cms.Api.Management.ViewModels.Installer;
|
||||
@@ -13,23 +12,16 @@ namespace Umbraco.Cms.Api.Management.Controllers.Install;
|
||||
[ApiVersion("1.0")]
|
||||
public class SettingsInstallController : InstallControllerBase
|
||||
{
|
||||
private readonly InstallHelper _installHelper;
|
||||
private readonly IInstallSettingsFactory _installSettingsFactory;
|
||||
private readonly IUmbracoMapper _mapper;
|
||||
|
||||
[Obsolete("Please use the constructor without the InstallHelper parameter. Scheduled for removal in Umbraco 19.")]
|
||||
public SettingsInstallController(
|
||||
InstallHelper installHelper,
|
||||
IInstallSettingsFactory installSettingsFactory,
|
||||
IUmbracoMapper mapper)
|
||||
: this(installSettingsFactory, mapper)
|
||||
{
|
||||
}
|
||||
|
||||
[ActivatorUtilitiesConstructor]
|
||||
public SettingsInstallController(
|
||||
IInstallSettingsFactory installSettingsFactory,
|
||||
IUmbracoMapper mapper)
|
||||
{
|
||||
_installHelper = installHelper;
|
||||
_installSettingsFactory = installSettingsFactory;
|
||||
_mapper = mapper;
|
||||
}
|
||||
@@ -40,6 +32,9 @@ public class SettingsInstallController : InstallControllerBase
|
||||
[ProducesResponseType(typeof(InstallSettingsResponseModel), StatusCodes.Status200OK)]
|
||||
public async Task<IActionResult> Settings(CancellationToken cancellationToken)
|
||||
{
|
||||
// Register that the install has started
|
||||
await _installHelper.SetInstallStatusAsync(false, string.Empty);
|
||||
|
||||
InstallSettingsModel installSettings = _installSettingsFactory.GetInstallSettings();
|
||||
InstallSettingsResponseModel responseModel = _mapper.Map<InstallSettingsResponseModel>(installSettings)!;
|
||||
|
||||
|
||||
+6
@@ -26,6 +26,12 @@ public abstract class MediaCollectionControllerBase : ContentCollectionControlle
|
||||
{
|
||||
}
|
||||
|
||||
[Obsolete("Please use the constructor with all parameters. Scheduled to be removed in Umbraco 18")]
|
||||
protected MediaCollectionControllerBase(IUmbracoMapper mapper)
|
||||
: base(mapper)
|
||||
{
|
||||
}
|
||||
|
||||
protected IActionResult CollectionOperationStatusResult(ContentCollectionOperationStatus status)
|
||||
=> ContentCollectionOperationStatusResult(status, "media");
|
||||
}
|
||||
|
||||
+11
-40
@@ -4,10 +4,8 @@ using Microsoft.AspNetCore.Mvc;
|
||||
using Umbraco.Cms.Api.Common.ViewModels.Pagination;
|
||||
using Umbraco.Cms.Api.Management.Factories;
|
||||
using Umbraco.Cms.Api.Management.ViewModels.TrackedReferences;
|
||||
using Umbraco.Cms.Core;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
using Umbraco.Cms.Core.Services.OperationStatus;
|
||||
|
||||
namespace Umbraco.Cms.Api.Management.Controllers.Media.References;
|
||||
|
||||
@@ -17,16 +15,22 @@ public class ReferencedByMediaController : MediaControllerBase
|
||||
private readonly ITrackedReferencesService _trackedReferencesService;
|
||||
private readonly IRelationTypePresentationFactory _relationTypePresentationFactory;
|
||||
|
||||
public ReferencedByMediaController(
|
||||
ITrackedReferencesService trackedReferencesService,
|
||||
IRelationTypePresentationFactory relationTypePresentationFactory)
|
||||
public ReferencedByMediaController(ITrackedReferencesService trackedReferencesService, IRelationTypePresentationFactory relationTypePresentationFactory)
|
||||
{
|
||||
_trackedReferencesService = trackedReferencesService;
|
||||
_relationTypePresentationFactory = relationTypePresentationFactory;
|
||||
}
|
||||
|
||||
[Obsolete("Use the ReferencedBy2 action method instead. Scheduled for removal in Umbraco 19, when ReferencedBy2 will be renamed back to ReferencedBy.")]
|
||||
[NonAction]
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// This is basically finding parents of relations.
|
||||
/// </remarks>
|
||||
[HttpGet("{id:guid}/referenced-by")]
|
||||
[MapToApiVersion("1.0")]
|
||||
[ProducesResponseType(typeof(PagedViewModel<IReferenceResponseModel>), StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<PagedViewModel<IReferenceResponseModel>>> ReferencedBy(
|
||||
CancellationToken cancellationToken,
|
||||
Guid id,
|
||||
@@ -43,37 +47,4 @@ public class ReferencedByMediaController : MediaControllerBase
|
||||
|
||||
return pagedViewModel;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// This is basically finding parents of relations.
|
||||
/// </remarks>
|
||||
[HttpGet("{id:guid}/referenced-by")]
|
||||
[MapToApiVersion("1.0")]
|
||||
[ProducesResponseType(typeof(PagedViewModel<IReferenceResponseModel>), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
|
||||
public async Task<IActionResult> ReferencedBy2(
|
||||
CancellationToken cancellationToken,
|
||||
Guid id,
|
||||
int skip = 0,
|
||||
int take = 20)
|
||||
{
|
||||
Attempt<PagedModel<RelationItemModel>, GetReferencesOperationStatus> relationItemsAttempt = await _trackedReferencesService.GetPagedRelationsForItemAsync(id, UmbracoObjectTypes.Media, skip, take, true);
|
||||
|
||||
if (relationItemsAttempt.Success is false)
|
||||
{
|
||||
return GetReferencesOperationStatusResult(relationItemsAttempt.Status);
|
||||
}
|
||||
|
||||
var pagedViewModel = new PagedViewModel<IReferenceResponseModel>
|
||||
{
|
||||
Total = relationItemsAttempt.Result.Total,
|
||||
Items = await _relationTypePresentationFactory.CreateReferenceResponseModelsAsync(relationItemsAttempt.Result.Items),
|
||||
};
|
||||
|
||||
return Ok(pagedViewModel);
|
||||
}
|
||||
}
|
||||
|
||||
+7
-35
@@ -3,11 +3,9 @@ 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;
|
||||
using Umbraco.Cms.Core.Services.OperationStatus;
|
||||
|
||||
namespace Umbraco.Cms.Api.Management.Controllers.Media.References;
|
||||
|
||||
@@ -16,32 +14,13 @@ public class ReferencedDescendantsMediaController : MediaControllerBase
|
||||
{
|
||||
private readonly ITrackedReferencesService _trackedReferencesSkipTakeService;
|
||||
private readonly IUmbracoMapper _umbracoMapper;
|
||||
public ReferencedDescendantsMediaController(
|
||||
ITrackedReferencesService trackedReferencesSkipTakeService,
|
||||
IUmbracoMapper umbracoMapper)
|
||||
|
||||
public ReferencedDescendantsMediaController(ITrackedReferencesService trackedReferencesSkipTakeService, IUmbracoMapper umbracoMapper)
|
||||
{
|
||||
_trackedReferencesSkipTakeService = trackedReferencesSkipTakeService;
|
||||
_umbracoMapper = umbracoMapper;
|
||||
}
|
||||
|
||||
[Obsolete("Use the ReferencedDescendants2 action method instead. Scheduled for removal in Umbraco 19, when ReferencedDescendants2 will be renamed back to ReferencedDescendants.")]
|
||||
[NonAction]
|
||||
public async Task<ActionResult<PagedViewModel<ReferenceByIdModel>>> ReferencedDescendants(
|
||||
CancellationToken cancellationToken,
|
||||
Guid id,
|
||||
int skip = 0,
|
||||
int take = 20)
|
||||
{
|
||||
PagedModel<RelationItemModel> relationItems = await _trackedReferencesSkipTakeService.GetPagedDescendantsInReferencesAsync(id, skip, take, true);
|
||||
var pagedViewModel = new PagedViewModel<ReferenceByIdModel>
|
||||
{
|
||||
Total = relationItems.Total,
|
||||
Items = _umbracoMapper.MapEnumerable<RelationItemModel, ReferenceByIdModel>(relationItems.Items),
|
||||
};
|
||||
|
||||
return pagedViewModel;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a page list of the child nodes of the current item used in any kind of relation.
|
||||
/// </summary>
|
||||
@@ -53,26 +32,19 @@ public class ReferencedDescendantsMediaController : MediaControllerBase
|
||||
[HttpGet("{id:guid}/referenced-descendants")]
|
||||
[MapToApiVersion("1.0")]
|
||||
[ProducesResponseType(typeof(PagedViewModel<ReferenceByIdModel>), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
|
||||
public async Task<IActionResult> ReferencedDescendants2(
|
||||
public async Task<ActionResult<PagedViewModel<ReferenceByIdModel>>> ReferencedDescendants(
|
||||
CancellationToken cancellationToken,
|
||||
Guid id,
|
||||
int skip = 0,
|
||||
int take = 20)
|
||||
{
|
||||
Attempt<PagedModel<RelationItemModel>, GetReferencesOperationStatus> relationItemsAttempt = await _trackedReferencesSkipTakeService.GetPagedDescendantsInReferencesAsync(id, UmbracoObjectTypes.Media, skip, take, true);
|
||||
|
||||
if (relationItemsAttempt.Success is false)
|
||||
{
|
||||
return GetReferencesOperationStatusResult(relationItemsAttempt.Status);
|
||||
}
|
||||
|
||||
PagedModel<RelationItemModel> relationItems = await _trackedReferencesSkipTakeService.GetPagedDescendantsInReferencesAsync(id, skip, take, true);
|
||||
var pagedViewModel = new PagedViewModel<ReferenceByIdModel>
|
||||
{
|
||||
Total = relationItemsAttempt.Result.Total,
|
||||
Items = _umbracoMapper.MapEnumerable<RelationItemModel, ReferenceByIdModel>(relationItemsAttempt.Result.Items),
|
||||
Total = relationItems.Total,
|
||||
Items = _umbracoMapper.MapEnumerable<RelationItemModel, ReferenceByIdModel>(relationItems.Items),
|
||||
};
|
||||
|
||||
return Ok(pagedViewModel);
|
||||
return pagedViewModel;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@ public abstract class MediaTypeControllerBase : ManagementApiControllerBase
|
||||
.WithDetail("The import failed due to not being able to convert the file into proper xml.")
|
||||
.Build()),
|
||||
MediaTypeImportOperationStatus.MediaTypeExists => BadRequest(problemDetailsBuilder
|
||||
.WithTitle("Failed to import because media type exists")
|
||||
.WithTitle("Failed to import because media type exits")
|
||||
.WithDetail("The import failed because the media type that was being imported already exits.")
|
||||
.Build()),
|
||||
MediaTypeImportOperationStatus.TypeMismatch => BadRequest(problemDetailsBuilder
|
||||
@@ -42,6 +42,6 @@ public abstract class MediaTypeControllerBase : ManagementApiControllerBase
|
||||
.WithTitle("Invalid Id")
|
||||
.WithDetail("The import failed because the id of the media type you are trying to update did not match the id in the file.")
|
||||
.Build()),
|
||||
_ => StatusCode(StatusCodes.Status500InternalServerError, "Unknown media type import operation status."),
|
||||
_ => StatusCode(StatusCodes.Status500InternalServerError, "Unknown media type import operation status.")
|
||||
});
|
||||
}
|
||||
|
||||
+4
-4
@@ -24,15 +24,15 @@ public class SearchMemberItemController : MemberItemControllerBase
|
||||
[HttpGet("search")]
|
||||
[MapToApiVersion("1.0")]
|
||||
[ProducesResponseType(typeof(PagedModel<MemberItemResponseModel>), StatusCodes.Status200OK)]
|
||||
public async Task<IActionResult> SearchWithAllowedTypes(CancellationToken cancellationToken, string query, int skip = 0, int take = 100, [FromQuery]IEnumerable<Guid>? allowedMemberTypes = null)
|
||||
public Task<IActionResult> SearchWithAllowedTypes(CancellationToken cancellationToken, string query, int skip = 0, int take = 100, [FromQuery]IEnumerable<Guid>? allowedMemberTypes = null)
|
||||
{
|
||||
PagedModel<IEntitySlim> searchResult = await _indexedEntitySearchService.SearchAsync(UmbracoObjectTypes.Member, query, null, allowedMemberTypes, false, "*", skip, take);
|
||||
PagedModel<IEntitySlim> searchResult = _indexedEntitySearchService.Search(UmbracoObjectTypes.Member, query, null, allowedMemberTypes, skip, take);
|
||||
var result = new PagedModel<MemberItemResponseModel>
|
||||
{
|
||||
Items = searchResult.Items.OfType<IMemberEntitySlim>().Select(_memberPresentationFactory.CreateItemResponseModel),
|
||||
Total = searchResult.Total,
|
||||
Total = searchResult.Total
|
||||
};
|
||||
|
||||
return await Task.FromResult<IActionResult>(Ok(result));
|
||||
return Task.FromResult<IActionResult>(Ok(result));
|
||||
}
|
||||
}
|
||||
|
||||
+11
-40
@@ -4,10 +4,8 @@ using Microsoft.AspNetCore.Mvc;
|
||||
using Umbraco.Cms.Api.Common.ViewModels.Pagination;
|
||||
using Umbraco.Cms.Api.Management.Factories;
|
||||
using Umbraco.Cms.Api.Management.ViewModels.TrackedReferences;
|
||||
using Umbraco.Cms.Core;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
using Umbraco.Cms.Core.Services.OperationStatus;
|
||||
|
||||
namespace Umbraco.Cms.Api.Management.Controllers.Member.References;
|
||||
|
||||
@@ -17,16 +15,22 @@ public class ReferencedByMemberController : MemberControllerBase
|
||||
private readonly ITrackedReferencesService _trackedReferencesService;
|
||||
private readonly IRelationTypePresentationFactory _relationTypePresentationFactory;
|
||||
|
||||
public ReferencedByMemberController(
|
||||
ITrackedReferencesService trackedReferencesService,
|
||||
IRelationTypePresentationFactory relationTypePresentationFactory)
|
||||
public ReferencedByMemberController(ITrackedReferencesService trackedReferencesService, IRelationTypePresentationFactory relationTypePresentationFactory)
|
||||
{
|
||||
_trackedReferencesService = trackedReferencesService;
|
||||
_relationTypePresentationFactory = relationTypePresentationFactory;
|
||||
}
|
||||
|
||||
[Obsolete("Use the ReferencedBy2 action method instead. Scheduled for removal in Umbraco 19, when ReferencedBy2 will be renamed back to ReferencedBy.")]
|
||||
[NonAction]
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// This is basically finding parents of relations.
|
||||
/// </remarks>
|
||||
[HttpGet("{id:guid}/referenced-by")]
|
||||
[MapToApiVersion("1.0")]
|
||||
[ProducesResponseType(typeof(PagedViewModel<IReferenceResponseModel>), StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<PagedViewModel<IReferenceResponseModel>>> ReferencedBy(
|
||||
CancellationToken cancellationToken,
|
||||
Guid id,
|
||||
@@ -43,37 +47,4 @@ public class ReferencedByMemberController : MemberControllerBase
|
||||
|
||||
return pagedViewModel;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// This is basically finding parents of relations.
|
||||
/// </remarks>
|
||||
[HttpGet("{id:guid}/referenced-by")]
|
||||
[MapToApiVersion("1.0")]
|
||||
[ProducesResponseType(typeof(PagedViewModel<IReferenceResponseModel>), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
|
||||
public async Task<IActionResult> ReferencedBy2(
|
||||
CancellationToken cancellationToken,
|
||||
Guid id,
|
||||
int skip = 0,
|
||||
int take = 20)
|
||||
{
|
||||
Attempt<PagedModel<RelationItemModel>, GetReferencesOperationStatus> relationItemsAttempt = await _trackedReferencesService.GetPagedRelationsForItemAsync(id, UmbracoObjectTypes.Member, skip, take, true);
|
||||
|
||||
if (relationItemsAttempt.Success is false)
|
||||
{
|
||||
return GetReferencesOperationStatusResult(relationItemsAttempt.Status);
|
||||
}
|
||||
|
||||
var pagedViewModel = new PagedViewModel<IReferenceResponseModel>
|
||||
{
|
||||
Total = relationItemsAttempt.Result.Total,
|
||||
Items = await _relationTypePresentationFactory.CreateReferenceResponseModelsAsync(relationItemsAttempt.Result.Items),
|
||||
};
|
||||
|
||||
return Ok(pagedViewModel);
|
||||
}
|
||||
}
|
||||
|
||||
+12
-41
@@ -3,11 +3,9 @@ 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;
|
||||
using Umbraco.Cms.Core.Services.OperationStatus;
|
||||
|
||||
namespace Umbraco.Cms.Api.Management.Controllers.Member.References;
|
||||
|
||||
@@ -17,16 +15,23 @@ public class ReferencedDescendantsMemberController : MemberControllerBase
|
||||
private readonly ITrackedReferencesService _trackedReferencesSkipTakeService;
|
||||
private readonly IUmbracoMapper _umbracoMapper;
|
||||
|
||||
public ReferencedDescendantsMemberController(
|
||||
ITrackedReferencesService trackedReferencesSkipTakeService,
|
||||
IUmbracoMapper umbracoMapper)
|
||||
public ReferencedDescendantsMemberController(ITrackedReferencesService trackedReferencesSkipTakeService, IUmbracoMapper umbracoMapper)
|
||||
{
|
||||
_trackedReferencesSkipTakeService = trackedReferencesSkipTakeService;
|
||||
_umbracoMapper = umbracoMapper;
|
||||
}
|
||||
|
||||
[Obsolete("Use the ReferencedDescendants2 action method instead. Scheduled for removal in Umbraco 19, when ReferencedDescendants2 will be renamed back to ReferencedDescendants.")]
|
||||
[NonAction]
|
||||
/// <summary>
|
||||
/// 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
|
||||
/// kind of relation.
|
||||
/// This is basically finding the descending items which are children in relations.
|
||||
/// </remarks>
|
||||
[HttpGet("{id:guid}/referenced-descendants")]
|
||||
[MapToApiVersion("1.0")]
|
||||
[ProducesResponseType(typeof(PagedViewModel<ReferenceByIdModel>), StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<PagedViewModel<ReferenceByIdModel>>> ReferencedDescendants(
|
||||
CancellationToken cancellationToken,
|
||||
Guid id,
|
||||
@@ -42,38 +47,4 @@ public class ReferencedDescendantsMemberController : MemberControllerBase
|
||||
|
||||
return pagedViewModel;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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
|
||||
/// kind of relation.
|
||||
/// This is basically finding the descending items which are children in relations.
|
||||
/// </remarks>
|
||||
[HttpGet("{id:guid}/referenced-descendants")]
|
||||
[MapToApiVersion("1.0")]
|
||||
[ProducesResponseType(typeof(PagedViewModel<ReferenceByIdModel>), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
|
||||
public async Task<IActionResult> ReferencedDescendants2(
|
||||
CancellationToken cancellationToken,
|
||||
Guid id,
|
||||
int skip = 0,
|
||||
int take = 20)
|
||||
{
|
||||
Attempt<PagedModel<RelationItemModel>, GetReferencesOperationStatus> relationItemsAttempt = await _trackedReferencesSkipTakeService.GetPagedDescendantsInReferencesAsync(id, UmbracoObjectTypes.Member, skip, take, true);
|
||||
|
||||
if (relationItemsAttempt.Success is false)
|
||||
{
|
||||
return GetReferencesOperationStatusResult(relationItemsAttempt.Status);
|
||||
}
|
||||
|
||||
var pagedViewModel = new PagedViewModel<ReferenceByIdModel>
|
||||
{
|
||||
Total = relationItemsAttempt.Result.Total,
|
||||
Items = _umbracoMapper.MapEnumerable<RelationItemModel, ReferenceByIdModel>(relationItemsAttempt.Result.Items),
|
||||
};
|
||||
|
||||
return Ok(pagedViewModel);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
using Asp.Versioning;
|
||||
using Asp.Versioning;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Umbraco.Cms.Api.Management.ViewModels.MemberType;
|
||||
using Umbraco.Cms.Core;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
@@ -20,23 +19,14 @@ public class CopyMemberTypeController : MemberTypeControllerBase
|
||||
public CopyMemberTypeController(IMemberTypeService memberTypeService)
|
||||
=> _memberTypeService = memberTypeService;
|
||||
|
||||
[Obsolete("Please use the overload that includes all parameters. Scheduled for removal in Umbraco 19.")]
|
||||
[NonAction]
|
||||
public async Task<IActionResult> Copy(
|
||||
CancellationToken cancellationToken,
|
||||
Guid id) => await Copy(cancellationToken, id, null);
|
||||
|
||||
[HttpPost("{id:guid}/copy")]
|
||||
[MapToApiVersion("1.0")]
|
||||
[ProducesResponseType(StatusCodes.Status201Created)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
|
||||
public async Task<IActionResult> Copy(
|
||||
CancellationToken cancellationToken,
|
||||
Guid id,
|
||||
CopyMemberTypeRequestModel? copyMemberTypeRequestModel)
|
||||
public async Task<IActionResult> Copy(CancellationToken cancellationToken, Guid id)
|
||||
{
|
||||
Attempt<IMemberType?, ContentTypeStructureOperationStatus> result = await _memberTypeService.CopyAsync(id, copyMemberTypeRequestModel?.Target?.Id);
|
||||
Attempt<IMemberType?, ContentTypeStructureOperationStatus> result = await _memberTypeService.CopyAsync(id, containerKey: null);
|
||||
|
||||
return result.Success
|
||||
? CreatedAtId<ByKeyMemberTypeController>(controller => nameof(controller.ByKey), result.Result!.Key)
|
||||
|
||||
@@ -1,44 +0,0 @@
|
||||
using Asp.Versioning;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Umbraco.Cms.Api.Management.Factories;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
using Umbraco.Cms.Core.Services.OperationStatus;
|
||||
using Umbraco.Cms.Web.Common.Authorization;
|
||||
|
||||
namespace Umbraco.Cms.Api.Management.Controllers.MemberType;
|
||||
|
||||
[ApiVersion("1.0")]
|
||||
[Authorize(Policy = AuthorizationPolicies.TreeAccessMemberTypes)]
|
||||
public class ExportMemberTypeController : MemberTypeControllerBase
|
||||
{
|
||||
private readonly IMemberTypeService _memberTypeService;
|
||||
private readonly IUdtFileContentFactory _fileContentFactory;
|
||||
|
||||
public ExportMemberTypeController(
|
||||
IMemberTypeService memberTypeService,
|
||||
IUdtFileContentFactory fileContentFactory)
|
||||
{
|
||||
_memberTypeService = memberTypeService;
|
||||
_fileContentFactory = fileContentFactory;
|
||||
}
|
||||
|
||||
[HttpGet("{id:guid}/export")]
|
||||
[MapToApiVersion("1.0")]
|
||||
[ProducesResponseType(typeof(FileContentResult), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
|
||||
public IActionResult Export(
|
||||
CancellationToken cancellationToken,
|
||||
Guid id)
|
||||
{
|
||||
IMemberType? memberType = _memberTypeService.Get(id);
|
||||
if (memberType is null)
|
||||
{
|
||||
return OperationStatusResult(ContentTypeOperationStatus.NotFound);
|
||||
}
|
||||
|
||||
return _fileContentFactory.Create(memberType);
|
||||
}
|
||||
}
|
||||
-25
@@ -1,25 +0,0 @@
|
||||
using Asp.Versioning;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Umbraco.Cms.Api.Management.ViewModels.Folder;
|
||||
using Umbraco.Cms.Core.Security;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
|
||||
namespace Umbraco.Cms.Api.Management.Controllers.MemberType.Folder;
|
||||
|
||||
[ApiVersion("1.0")]
|
||||
public class ByKeyMemberTypeFolderController : MemberTypeFolderControllerBase
|
||||
{
|
||||
public ByKeyMemberTypeFolderController(
|
||||
IBackOfficeSecurityAccessor backOfficeSecurityAccessor,
|
||||
IMemberTypeContainerService memberTypeContainerService)
|
||||
: base(backOfficeSecurityAccessor, memberTypeContainerService)
|
||||
{
|
||||
}
|
||||
|
||||
[HttpGet("{id:guid}")]
|
||||
[MapToApiVersion("1.0")]
|
||||
[ProducesResponseType(typeof(FolderResponseModel), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
|
||||
public async Task<IActionResult> ByKey(CancellationToken cancellationToken, Guid id) => await GetFolderAsync(id);
|
||||
}
|
||||
-31
@@ -1,31 +0,0 @@
|
||||
using Asp.Versioning;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Umbraco.Cms.Api.Management.ViewModels.Folder;
|
||||
using Umbraco.Cms.Core.Security;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
|
||||
namespace Umbraco.Cms.Api.Management.Controllers.MemberType.Folder;
|
||||
|
||||
[ApiVersion("1.0")]
|
||||
public class CreateMemberTypeFolderController : MemberTypeFolderControllerBase
|
||||
{
|
||||
public CreateMemberTypeFolderController(
|
||||
IBackOfficeSecurityAccessor backOfficeSecurityAccessor,
|
||||
IMemberTypeContainerService memberTypeContainerService)
|
||||
: base(backOfficeSecurityAccessor, memberTypeContainerService)
|
||||
{
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[MapToApiVersion("1.0")]
|
||||
[ProducesResponseType(StatusCodes.Status201Created)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
|
||||
public async Task<IActionResult> Create(
|
||||
CancellationToken cancellationToken,
|
||||
CreateFolderRequestModel createFolderRequestModel)
|
||||
=> await CreateFolderAsync<ByKeyMemberTypeFolderController>(
|
||||
createFolderRequestModel,
|
||||
controller => nameof(controller.ByKey)).ConfigureAwait(false);
|
||||
}
|
||||
-25
@@ -1,25 +0,0 @@
|
||||
using Asp.Versioning;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Umbraco.Cms.Core.Security;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
|
||||
namespace Umbraco.Cms.Api.Management.Controllers.MemberType.Folder;
|
||||
|
||||
[ApiVersion("1.0")]
|
||||
public class DeleteMemberTypeFolderController : MemberTypeFolderControllerBase
|
||||
{
|
||||
public DeleteMemberTypeFolderController(
|
||||
IBackOfficeSecurityAccessor backOfficeSecurityAccessor,
|
||||
IMemberTypeContainerService memberTypeContainerService)
|
||||
: base(backOfficeSecurityAccessor, memberTypeContainerService)
|
||||
{
|
||||
}
|
||||
|
||||
[HttpDelete("{id:guid}")]
|
||||
[MapToApiVersion("1.0")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
|
||||
public async Task<IActionResult> Delete(CancellationToken cancellationToken, Guid id) => await DeleteFolderAsync(id);
|
||||
}
|
||||
-23
@@ -1,23 +0,0 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Umbraco.Cms.Api.Management.Routing;
|
||||
using Umbraco.Cms.Core;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Security;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
using Umbraco.Cms.Web.Common.Authorization;
|
||||
|
||||
namespace Umbraco.Cms.Api.Management.Controllers.MemberType.Folder;
|
||||
|
||||
[VersionedApiBackOfficeRoute($"{Constants.UdiEntityType.MemberType}/folder")]
|
||||
[ApiExplorerSettings(GroupName = "Member Type")]
|
||||
[Authorize(Policy = AuthorizationPolicies.TreeAccessMemberTypes)]
|
||||
public abstract class MemberTypeFolderControllerBase : FolderManagementControllerBase<IMemberType>
|
||||
{
|
||||
protected MemberTypeFolderControllerBase(
|
||||
IBackOfficeSecurityAccessor backOfficeSecurityAccessor,
|
||||
IMemberTypeContainerService memberTypeContainerService)
|
||||
: base(backOfficeSecurityAccessor, memberTypeContainerService)
|
||||
{
|
||||
}
|
||||
}
|
||||
-30
@@ -1,30 +0,0 @@
|
||||
using Asp.Versioning;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Umbraco.Cms.Api.Management.ViewModels.Folder;
|
||||
using Umbraco.Cms.Core.Security;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
|
||||
namespace Umbraco.Cms.Api.Management.Controllers.MemberType.Folder;
|
||||
|
||||
[ApiVersion("1.0")]
|
||||
public class UpdateMemberTypeFolderController : MemberTypeFolderControllerBase
|
||||
{
|
||||
public UpdateMemberTypeFolderController(
|
||||
IBackOfficeSecurityAccessor backOfficeSecurityAccessor,
|
||||
IMemberTypeContainerService memberTypeContainerService)
|
||||
: base(backOfficeSecurityAccessor, memberTypeContainerService)
|
||||
{
|
||||
}
|
||||
|
||||
[HttpPut("{id:guid}")]
|
||||
[MapToApiVersion("1.0")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
|
||||
public async Task<IActionResult> Update(
|
||||
CancellationToken cancellationToken,
|
||||
Guid id,
|
||||
UpdateFolderResponseModel updateFolderResponseModel)
|
||||
=> await UpdateFolderAsync(id, updateFolderResponseModel);
|
||||
}
|
||||
-46
@@ -1,46 +0,0 @@
|
||||
using Asp.Versioning;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Umbraco.Cms.Api.Management.ViewModels.MemberType;
|
||||
using Umbraco.Cms.Core;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Security;
|
||||
using Umbraco.Cms.Core.Services.ImportExport;
|
||||
using Umbraco.Cms.Core.Services.OperationStatus;
|
||||
using Umbraco.Cms.Web.Common.Authorization;
|
||||
|
||||
namespace Umbraco.Cms.Api.Management.Controllers.MemberType;
|
||||
|
||||
[ApiVersion("1.0")]
|
||||
[Authorize(Policy = AuthorizationPolicies.TreeAccessMemberTypes)]
|
||||
public class ImportExistingMemberTypeController : MemberTypeControllerBase
|
||||
{
|
||||
private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor;
|
||||
private readonly IMemberTypeImportService _memberTypeImportService;
|
||||
|
||||
public ImportExistingMemberTypeController(
|
||||
IBackOfficeSecurityAccessor backOfficeSecurityAccessor,
|
||||
IMemberTypeImportService memberTypeImportService)
|
||||
{
|
||||
_backOfficeSecurityAccessor = backOfficeSecurityAccessor;
|
||||
_memberTypeImportService = memberTypeImportService;
|
||||
}
|
||||
|
||||
[HttpPut("{id:guid}/import")]
|
||||
[MapToApiVersion("1.0")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)]
|
||||
public async Task<IActionResult> Import(
|
||||
CancellationToken cancellationToken,
|
||||
Guid id,
|
||||
ImportMemberTypeRequestModel model)
|
||||
{
|
||||
Attempt<IMemberType?, MemberTypeImportOperationStatus> importAttempt = await _memberTypeImportService.Import(model.File.Id, CurrentUserKey(_backOfficeSecurityAccessor));
|
||||
|
||||
return importAttempt.Success is false
|
||||
? MemberTypeImportOperationStatusResult(importAttempt.Status)
|
||||
: Ok();
|
||||
}
|
||||
}
|
||||
-45
@@ -1,45 +0,0 @@
|
||||
using Asp.Versioning;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Umbraco.Cms.Api.Management.ViewModels.MemberType;
|
||||
using Umbraco.Cms.Core;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Security;
|
||||
using Umbraco.Cms.Core.Services.ImportExport;
|
||||
using Umbraco.Cms.Core.Services.OperationStatus;
|
||||
using Umbraco.Cms.Web.Common.Authorization;
|
||||
|
||||
namespace Umbraco.Cms.Api.Management.Controllers.MemberType;
|
||||
|
||||
[ApiVersion("1.0")]
|
||||
[Authorize(Policy = AuthorizationPolicies.TreeAccessMemberTypes)]
|
||||
public class ImportNewMemberTypeController : MemberTypeControllerBase
|
||||
{
|
||||
private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor;
|
||||
private readonly IMemberTypeImportService _memberTypeImportService;
|
||||
|
||||
public ImportNewMemberTypeController(
|
||||
IBackOfficeSecurityAccessor backOfficeSecurityAccessor,
|
||||
IMemberTypeImportService memberTypeImportService)
|
||||
{
|
||||
_backOfficeSecurityAccessor = backOfficeSecurityAccessor;
|
||||
_memberTypeImportService = memberTypeImportService;
|
||||
}
|
||||
|
||||
[HttpPost("import")]
|
||||
[MapToApiVersion("1.0")]
|
||||
[ProducesResponseType(StatusCodes.Status201Created)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)]
|
||||
public async Task<IActionResult> Import(
|
||||
CancellationToken cancellationToken,
|
||||
ImportMemberTypeRequestModel model)
|
||||
{
|
||||
Attempt<IMemberType?, MemberTypeImportOperationStatus> importAttempt = await _memberTypeImportService.Import(model.File.Id, CurrentUserKey(_backOfficeSecurityAccessor));
|
||||
|
||||
return importAttempt.Success is false
|
||||
? MemberTypeImportOperationStatusResult(importAttempt.Status)
|
||||
: CreatedAtId<ByKeyMemberTypeController>(controller => nameof(controller.ByKey), importAttempt.Result!.Key);
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Umbraco.Cms.Api.Management.Controllers.DocumentType;
|
||||
using Umbraco.Cms.Api.Management.Routing;
|
||||
@@ -19,29 +18,4 @@ public abstract class MemberTypeControllerBase : ManagementApiControllerBase
|
||||
|
||||
protected IActionResult StructureOperationStatusResult(ContentTypeStructureOperationStatus status)
|
||||
=> DocumentTypeControllerBase.ContentTypeStructureOperationStatusResult(status, "member");
|
||||
|
||||
protected IActionResult MemberTypeImportOperationStatusResult(MemberTypeImportOperationStatus operationStatus) =>
|
||||
OperationStatusResult(operationStatus, problemDetailsBuilder => operationStatus switch
|
||||
{
|
||||
MemberTypeImportOperationStatus.TemporaryFileNotFound => NotFound(problemDetailsBuilder
|
||||
.WithTitle("Temporary file not found")
|
||||
.Build()),
|
||||
MemberTypeImportOperationStatus.TemporaryFileConversionFailure => BadRequest(problemDetailsBuilder
|
||||
.WithTitle("Failed to convert the specified file")
|
||||
.WithDetail("The import failed due to not being able to convert the file into proper xml.")
|
||||
.Build()),
|
||||
MemberTypeImportOperationStatus.MemberTypeExists => BadRequest(problemDetailsBuilder
|
||||
.WithTitle("Failed to import because member type exists")
|
||||
.WithDetail("The import failed because the member type that was being imported already exits.")
|
||||
.Build()),
|
||||
MemberTypeImportOperationStatus.TypeMismatch => BadRequest(problemDetailsBuilder
|
||||
.WithTitle("Type Mismatch")
|
||||
.WithDetail("The import failed because the file contained an entity that is not a member type.")
|
||||
.Build()),
|
||||
MemberTypeImportOperationStatus.IdMismatch => BadRequest(problemDetailsBuilder
|
||||
.WithTitle("Invalid Id")
|
||||
.WithDetail("The import failed because the id of the member type you are trying to update did not match the id in the file.")
|
||||
.Build()),
|
||||
_ => StatusCode(StatusCodes.Status500InternalServerError, "Unknown member type import operation status."),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
using Asp.Versioning;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Umbraco.Cms.Api.Management.ViewModels.MemberType;
|
||||
using Umbraco.Cms.Core;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
using Umbraco.Cms.Core.Services.OperationStatus;
|
||||
using Umbraco.Cms.Web.Common.Authorization;
|
||||
|
||||
namespace Umbraco.Cms.Api.Management.Controllers.MemberType;
|
||||
|
||||
[ApiVersion("1.0")]
|
||||
[Authorize(Policy = AuthorizationPolicies.TreeAccessMemberTypes)]
|
||||
public class MoveMemberTypeController : MemberTypeControllerBase
|
||||
{
|
||||
private readonly IMemberTypeService _memberTypeService;
|
||||
|
||||
public MoveMemberTypeController(IMemberTypeService memberTypeService)
|
||||
=> _memberTypeService = memberTypeService;
|
||||
|
||||
[HttpPut("{id:guid}/move")]
|
||||
[MapToApiVersion("1.0")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
|
||||
public async Task<IActionResult> Move(
|
||||
CancellationToken cancellationToken,
|
||||
Guid id,
|
||||
MoveMemberTypeRequestModel moveMemberTypeRequestModel)
|
||||
{
|
||||
Attempt<IMemberType?, ContentTypeStructureOperationStatus> result = await _memberTypeService.MoveAsync(id, moveMemberTypeRequestModel.Target?.Id);
|
||||
|
||||
return result.Success
|
||||
? Ok()
|
||||
: StructureOperationStatusResult(result.Status);
|
||||
}
|
||||
}
|
||||
-23
@@ -1,23 +0,0 @@
|
||||
using Asp.Versioning;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Umbraco.Cms.Api.Management.Services.Flags;
|
||||
using Umbraco.Cms.Api.Management.ViewModels.Tree;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
|
||||
namespace Umbraco.Cms.Api.Management.Controllers.MemberType.Tree;
|
||||
|
||||
[ApiVersion("1.0")]
|
||||
public class AncestorsMemberTypeTreeController : MemberTypeTreeControllerBase
|
||||
{
|
||||
public AncestorsMemberTypeTreeController(IEntityService entityService, FlagProviderCollection flagProviders, IMemberTypeService memberTypeService)
|
||||
: base(entityService, flagProviders, memberTypeService)
|
||||
{
|
||||
}
|
||||
|
||||
[HttpGet("ancestors")]
|
||||
[MapToApiVersion("1.0")]
|
||||
[ProducesResponseType(typeof(IEnumerable<MemberTypeTreeItemResponseModel>), StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<IEnumerable<MemberTypeTreeItemResponseModel>>> Ancestors(CancellationToken cancellationToken, Guid descendantId)
|
||||
=> await GetAncestors(descendantId);
|
||||
}
|
||||
-31
@@ -1,31 +0,0 @@
|
||||
using Asp.Versioning;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Umbraco.Cms.Api.Common.ViewModels.Pagination;
|
||||
using Umbraco.Cms.Api.Management.Services.Flags;
|
||||
using Umbraco.Cms.Api.Management.ViewModels.Tree;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
|
||||
namespace Umbraco.Cms.Api.Management.Controllers.MemberType.Tree;
|
||||
|
||||
[ApiVersion("1.0")]
|
||||
public class ChildrenMemberTypeTreeController : MemberTypeTreeControllerBase
|
||||
{
|
||||
public ChildrenMemberTypeTreeController(IEntityService entityService, FlagProviderCollection flagProviders, IMemberTypeService memberTypeService)
|
||||
: base(entityService, flagProviders, memberTypeService)
|
||||
{ }
|
||||
|
||||
[HttpGet("children")]
|
||||
[MapToApiVersion("1.0")]
|
||||
[ProducesResponseType(typeof(PagedViewModel<MemberTypeTreeItemResponseModel>), StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<PagedViewModel<MemberTypeTreeItemResponseModel>>> Children(
|
||||
CancellationToken cancellationToken,
|
||||
Guid parentId,
|
||||
int skip = 0,
|
||||
int take = 100,
|
||||
bool foldersOnly = false)
|
||||
{
|
||||
RenderFoldersOnly(foldersOnly);
|
||||
return await GetChildren(parentId, skip, take);
|
||||
}
|
||||
}
|
||||
+1
-3
@@ -17,7 +17,7 @@ namespace Umbraco.Cms.Api.Management.Controllers.MemberType.Tree;
|
||||
[VersionedApiBackOfficeRoute($"{Constants.Web.RoutePath.Tree}/{Constants.UdiEntityType.MemberType}")]
|
||||
[ApiExplorerSettings(GroupName = "Member Type")]
|
||||
[Authorize(Policy = AuthorizationPolicies.TreeAccessMembersOrMemberTypes)]
|
||||
public class MemberTypeTreeControllerBase : FolderTreeControllerBase<MemberTypeTreeItemResponseModel>
|
||||
public class MemberTypeTreeControllerBase : NamedEntityTreeControllerBase<MemberTypeTreeItemResponseModel>
|
||||
{
|
||||
private readonly IMemberTypeService _memberTypeService;
|
||||
|
||||
@@ -37,8 +37,6 @@ public class MemberTypeTreeControllerBase : FolderTreeControllerBase<MemberTypeT
|
||||
|
||||
protected override UmbracoObjectTypes ItemObjectType => UmbracoObjectTypes.MemberType;
|
||||
|
||||
protected override UmbracoObjectTypes FolderObjectType => UmbracoObjectTypes.MemberTypeContainer;
|
||||
|
||||
protected override MemberTypeTreeItemResponseModel[] MapTreeItemViewModels(Guid? parentKey, IEntitySlim[] entities)
|
||||
{
|
||||
var memberTypes = _memberTypeService
|
||||
|
||||
+2
-6
@@ -30,10 +30,6 @@ public class RootMemberTypeTreeController : MemberTypeTreeControllerBase
|
||||
public async Task<ActionResult<PagedViewModel<MemberTypeTreeItemResponseModel>>> Root(
|
||||
CancellationToken cancellationToken,
|
||||
int skip = 0,
|
||||
int take = 100,
|
||||
bool foldersOnly = false)
|
||||
{
|
||||
RenderFoldersOnly(foldersOnly);
|
||||
return await GetRoot(skip, take);
|
||||
}
|
||||
int take = 100)
|
||||
=> await GetRoot(skip, take);
|
||||
}
|
||||
|
||||
+7
-16
@@ -1,6 +1,5 @@
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Umbraco.Cms.Api.Common.ViewModels.Pagination;
|
||||
using Umbraco.Cms.Api.Management.Services.Flags;
|
||||
using Umbraco.Cms.Api.Management.ViewModels.Tree;
|
||||
@@ -10,14 +9,10 @@ namespace Umbraco.Cms.Api.Management.Controllers.MemberType.Tree;
|
||||
|
||||
public class SiblingMemberTypeTreeController : MemberTypeTreeControllerBase
|
||||
{
|
||||
[Obsolete("Please use the constructor taking all parameters. Scheduled for removal in Umbraco 18.")]
|
||||
public SiblingMemberTypeTreeController(IEntityService entityService, IMemberTypeService memberTypeService)
|
||||
: base(entityService, memberTypeService)
|
||||
{
|
||||
}
|
||||
|
||||
[ActivatorUtilitiesConstructor]
|
||||
public SiblingMemberTypeTreeController(IEntityService entityService, FlagProviderCollection flagProviders, IMemberTypeService memberTypeService)
|
||||
public SiblingMemberTypeTreeController(
|
||||
IEntityService entityService,
|
||||
FlagProviderCollection flagProviders,
|
||||
IMemberTypeService memberTypeService)
|
||||
: base(entityService, flagProviders, memberTypeService)
|
||||
{
|
||||
}
|
||||
@@ -28,10 +23,6 @@ public class SiblingMemberTypeTreeController : MemberTypeTreeControllerBase
|
||||
CancellationToken cancellationToken,
|
||||
Guid target,
|
||||
int before,
|
||||
int after,
|
||||
bool foldersOnly = false)
|
||||
{
|
||||
RenderFoldersOnly(foldersOnly);
|
||||
return await GetSiblings(target, before, after);
|
||||
}
|
||||
int after)
|
||||
=> await GetSiblings(target, before, after);
|
||||
}
|
||||
|
||||
+8
-4
@@ -1,18 +1,21 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Asp.Versioning;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Umbraco.Cms.Core;
|
||||
using Umbraco.Cms.Core.Configuration.Models;
|
||||
using Umbraco.Cms.Infrastructure.ModelsBuilder;
|
||||
using Umbraco.Cms.Infrastructure.ModelsBuilder.Building;
|
||||
using Umbraco.Extensions;
|
||||
|
||||
namespace Umbraco.Cms.Api.Management.Controllers.ModelsBuilder;
|
||||
|
||||
[ApiVersion("1.0")]
|
||||
public class BuildModelsBuilderController : ModelsBuilderControllerBase
|
||||
{
|
||||
private readonly ModelsBuilderSettings _modelsBuilderSettings;
|
||||
private ModelsBuilderSettings _modelsBuilderSettings;
|
||||
private readonly ModelsGenerationError _mbErrors;
|
||||
private readonly IModelsGenerator _modelGenerator;
|
||||
|
||||
@@ -24,6 +27,8 @@ public class BuildModelsBuilderController : ModelsBuilderControllerBase
|
||||
_mbErrors = mbErrors;
|
||||
_modelGenerator = modelGenerator;
|
||||
_modelsBuilderSettings = modelsBuilderSettings.CurrentValue;
|
||||
|
||||
modelsBuilderSettings.OnChange(x => _modelsBuilderSettings = x);
|
||||
}
|
||||
|
||||
[HttpPost("build")]
|
||||
@@ -34,8 +39,7 @@ public class BuildModelsBuilderController : ModelsBuilderControllerBase
|
||||
{
|
||||
try
|
||||
{
|
||||
if (_modelsBuilderSettings.ModelsMode != Constants.ModelsBuilder.ModelsModes.SourceCodeManual
|
||||
&& _modelsBuilderSettings.ModelsMode != Constants.ModelsBuilder.ModelsModes.SourceCodeAuto)
|
||||
if (!_modelsBuilderSettings.ModelsMode.SupportsExplicitGeneration())
|
||||
{
|
||||
var problemDetailsModel = new ProblemDetails
|
||||
{
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Umbraco.Cms.Api.Management.Services.NewsDashboard;
|
||||
using Umbraco.Cms.Api.Management.ViewModels.NewsDashboard;
|
||||
|
||||
namespace Umbraco.Cms.Api.Management.Controllers.NewsDashboard;
|
||||
|
||||
public class NewsDashboardController : NewsDashboardControllerBase
|
||||
{
|
||||
private readonly INewsDashboardService _newsDashboardService;
|
||||
|
||||
public NewsDashboardController(INewsDashboardService newsDashboardService) => _newsDashboardService = newsDashboardService;
|
||||
|
||||
[HttpGet]
|
||||
[ProducesResponseType(typeof(NewsDashboardResponseModel), StatusCodes.Status200OK)]
|
||||
public async Task<IActionResult> GetDashboard()
|
||||
{
|
||||
NewsDashboardResponseModel content = await _newsDashboardService.GetItemsAsync();
|
||||
|
||||
return Ok(content);
|
||||
}
|
||||
}
|
||||
-10
@@ -1,10 +0,0 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Umbraco.Cms.Api.Management.Routing;
|
||||
|
||||
namespace Umbraco.Cms.Api.Management.Controllers.NewsDashboard;
|
||||
|
||||
[VersionedApiBackOfficeRoute("news-dashboard")]
|
||||
[ApiExplorerSettings(GroupName = "News Dashboard")]
|
||||
public abstract class NewsDashboardControllerBase : ManagementApiControllerBase
|
||||
{
|
||||
}
|
||||
@@ -7,7 +7,6 @@ using Umbraco.Cms.Core.Services;
|
||||
namespace Umbraco.Cms.Api.Management.Controllers.Preview;
|
||||
|
||||
[ApiVersion("1.0")]
|
||||
[Obsolete("Do not use this. Preview state is initiated implicitly by the preview URL generation. Scheduled for removal in V18.")]
|
||||
public class EnterPreviewController : PreviewControllerBase
|
||||
{
|
||||
private readonly IPreviewService _previewService;
|
||||
|
||||
@@ -254,7 +254,7 @@ public class BackOfficeController : SecurityControllerBase
|
||||
if (associatedUser is not null)
|
||||
{
|
||||
// log current datetime as last login (this also ensures that the user is not flagged as inactive)
|
||||
associatedUser.LastLoginDate = DateTime.UtcNow;
|
||||
associatedUser.LastLoginDateUtc = DateTime.UtcNow;
|
||||
await _backOfficeUserManager.UpdateAsync(associatedUser);
|
||||
|
||||
return await SignInBackOfficeUser(associatedUser, request);
|
||||
|
||||
@@ -17,6 +17,12 @@ public class BackOfficeDefaultController : Controller
|
||||
public BackOfficeDefaultController(IRuntime umbracoRuntime)
|
||||
=> _umbracoRuntime = umbracoRuntime;
|
||||
|
||||
[Obsolete("Use the non obsoleted constructor instead. Scheduled to be removed in v17")]
|
||||
public BackOfficeDefaultController()
|
||||
: this(StaticServiceProvider.Instance.GetRequiredService<IRuntime>())
|
||||
{
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[AllowAnonymous]
|
||||
public async Task<IActionResult> Index(CancellationToken cancellationToken)
|
||||
|
||||
@@ -13,7 +13,6 @@ namespace Umbraco.Cms.Api.Management.Controllers.Server;
|
||||
|
||||
[ApiVersion("1.0")]
|
||||
[Authorize(Policy = AuthorizationPolicies.RequireAdminAccess)]
|
||||
[Obsolete("Upgrade checks are no longer supported and this controller will be removed in Umbraco 19.")]
|
||||
public class UpgradeCheckServerController : ServerControllerBase
|
||||
{
|
||||
private readonly IUpgradeService _upgradeService;
|
||||
@@ -28,7 +27,6 @@ public class UpgradeCheckServerController : ServerControllerBase
|
||||
[HttpGet("upgrade-check")]
|
||||
[MapToApiVersion("1.0")]
|
||||
[ProducesResponseType(typeof(UpgradeCheckResponseModel), StatusCodes.Status200OK)]
|
||||
[Obsolete("Upgrade checks are no longer supported and this endpoint will be removed in Umbraco 19.")]
|
||||
public async Task<IActionResult> UpgradeCheck(CancellationToken cancellationToken)
|
||||
{
|
||||
UpgradeResult upgradeResult = await _upgradeService.CheckUpgrade(_umbracoVersion.SemanticVersion);
|
||||
|
||||
+2
-2
@@ -34,14 +34,14 @@ public class StaticFileTreeControllerBase : FileSystemTreeControllerBase
|
||||
|
||||
protected override IFileSystem FileSystem { get; }
|
||||
|
||||
protected override string[] GetDirectories(string path) =>
|
||||
protected string[] GetDirectories(string path) =>
|
||||
IsTreeRootPath(path)
|
||||
? _allowedRootFolders
|
||||
: IsAllowedPath(path)
|
||||
? _fileSystemTreeService.GetDirectories(path)
|
||||
: Array.Empty<string>();
|
||||
|
||||
protected override string[] GetFiles(string path)
|
||||
protected string[] GetFiles(string path)
|
||||
=> IsTreeRootPath(path) || IsAllowedPath(path) == false
|
||||
? Array.Empty<string>()
|
||||
: _fileSystemTreeService.GetFiles(path);
|
||||
|
||||
+35
@@ -43,6 +43,41 @@ public class ExecuteTemplateQueryController : TemplateQueryControllerBase
|
||||
_publishedContentStatusFilteringService = publishedContentStatusFilteringService;
|
||||
}
|
||||
|
||||
[Obsolete("Please use the non-obsolete constructor. Will be removed in V17.")]
|
||||
public ExecuteTemplateQueryController(
|
||||
IPublishedContentQuery publishedContentQuery,
|
||||
IVariationContextAccessor variationContextAccessor,
|
||||
IPublishedValueFallback publishedValueFallback,
|
||||
IContentTypeService contentTypeService,
|
||||
IPublishedContentCache contentCache,
|
||||
IDocumentNavigationQueryService documentNavigationQueryService,
|
||||
IPublishedContentStatusFilteringService publishedContentStatusFilteringService)
|
||||
: this(
|
||||
publishedContentQuery,
|
||||
publishedValueFallback,
|
||||
contentTypeService,
|
||||
documentNavigationQueryService,
|
||||
publishedContentStatusFilteringService)
|
||||
{
|
||||
}
|
||||
|
||||
[Obsolete("Please use the non-obsolete constructor. Will be removed in V17.")]
|
||||
public ExecuteTemplateQueryController(
|
||||
IPublishedContentQuery publishedContentQuery,
|
||||
IVariationContextAccessor variationContextAccessor,
|
||||
IPublishedValueFallback publishedValueFallback,
|
||||
IContentTypeService contentTypeService,
|
||||
IPublishedContentCache contentCache,
|
||||
IDocumentNavigationQueryService documentNavigationQueryService)
|
||||
: this(
|
||||
publishedContentQuery,
|
||||
publishedValueFallback,
|
||||
contentTypeService,
|
||||
documentNavigationQueryService,
|
||||
StaticServiceProvider.Instance.GetRequiredService<IPublishedContentStatusFilteringService>())
|
||||
{
|
||||
}
|
||||
|
||||
[HttpPost("execute")]
|
||||
[MapToApiVersion("1.0")]
|
||||
[ProducesResponseType(typeof(TemplateQueryResultResponseModel), StatusCodes.Status200OK)]
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
using Asp.Versioning;
|
||||
using Asp.Versioning;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Umbraco.Cms.Api.Management.Controllers.UserGroup;
|
||||
using Umbraco.Cms.Api.Management.Routing;
|
||||
using Umbraco.Cms.Api.Management.ViewModels.User;
|
||||
using Umbraco.Cms.Core;
|
||||
using Umbraco.Cms.Core.DependencyInjection;
|
||||
using Umbraco.Cms.Core.Security;
|
||||
using Umbraco.Cms.Core.Security.Authorization;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
using Umbraco.Cms.Core.Services.OperationStatus;
|
||||
@@ -25,11 +28,26 @@ public class UpdateUserGroupsUserController : UserGroupControllerBase
|
||||
{
|
||||
private readonly IAuthorizationService _authorizationService;
|
||||
private readonly IUserGroupService _userGroupService;
|
||||
private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor;
|
||||
|
||||
public UpdateUserGroupsUserController(IAuthorizationService authorizationService, IUserGroupService userGroupService)
|
||||
[ActivatorUtilitiesConstructor]
|
||||
public UpdateUserGroupsUserController(
|
||||
IAuthorizationService authorizationService,
|
||||
IUserGroupService userGroupService,
|
||||
IBackOfficeSecurityAccessor backOfficeSecurityAccessor)
|
||||
{
|
||||
_authorizationService = authorizationService;
|
||||
_userGroupService = userGroupService;
|
||||
_backOfficeSecurityAccessor = backOfficeSecurityAccessor;
|
||||
}
|
||||
|
||||
[Obsolete("Please use the constructor accepting all parameters. Scheduled for removal in Umbraco 19.")]
|
||||
public UpdateUserGroupsUserController(IAuthorizationService authorizationService, IUserGroupService userGroupService)
|
||||
: this(
|
||||
authorizationService,
|
||||
userGroupService,
|
||||
StaticServiceProvider.Instance.GetRequiredService<IBackOfficeSecurityAccessor>())
|
||||
{
|
||||
}
|
||||
|
||||
[HttpPost("set-user-groups")]
|
||||
@@ -51,7 +69,8 @@ public class UpdateUserGroupsUserController : UserGroupControllerBase
|
||||
|
||||
Attempt<UserGroupOperationStatus> result = await _userGroupService.UpdateUserGroupsOnUsersAsync(
|
||||
requestModel.UserGroupIds.Select(x => x.Id).ToHashSet(),
|
||||
requestModel.UserIds.Select(x => x.Id).ToHashSet());
|
||||
requestModel.UserIds.Select(x => x.Id).ToHashSet(),
|
||||
CurrentUserKey(_backOfficeSecurityAccessor));
|
||||
|
||||
return result.Success
|
||||
? Ok()
|
||||
|
||||
@@ -21,10 +21,6 @@ public abstract class WebhookControllerBase : ManagementApiControllerBase
|
||||
.WithTitle("Cancelled by notification")
|
||||
.WithDetail("A notification handler prevented the webhook operation.")
|
||||
.Build()),
|
||||
WebhookOperationStatus.NoEvents => BadRequest(new ProblemDetailsBuilder()
|
||||
.WithTitle("No events specified")
|
||||
.WithDetail("The webhook must be configured to listen to at least one event.")
|
||||
.Build()),
|
||||
_ => StatusCode(StatusCodes.Status500InternalServerError, new ProblemDetailsBuilder()
|
||||
.WithTitle("Unknown webhook operation status.")
|
||||
.Build()),
|
||||
|
||||
@@ -11,13 +11,13 @@ internal static class AuditLogBuilderExtensions
|
||||
internal static IUmbracoBuilder AddAuditLogs(this IUmbracoBuilder builder)
|
||||
{
|
||||
builder.Services.AddTransient<IAuditLogPresentationFactory, AuditLogPresentationFactory>();
|
||||
builder.AddNotificationAsyncHandler<UserLoginSuccessNotification, BackOfficeUserManagerAuditer>();
|
||||
builder.AddNotificationAsyncHandler<UserLogoutSuccessNotification, BackOfficeUserManagerAuditer>();
|
||||
builder.AddNotificationAsyncHandler<UserLoginFailedNotification, BackOfficeUserManagerAuditer>();
|
||||
builder.AddNotificationAsyncHandler<UserForgotPasswordRequestedNotification, BackOfficeUserManagerAuditer>();
|
||||
builder.AddNotificationAsyncHandler<UserForgotPasswordChangedNotification, BackOfficeUserManagerAuditer>();
|
||||
builder.AddNotificationAsyncHandler<UserPasswordChangedNotification, BackOfficeUserManagerAuditer>();
|
||||
builder.AddNotificationAsyncHandler<UserPasswordResetNotification, BackOfficeUserManagerAuditer>();
|
||||
builder.AddNotificationHandler<UserLoginSuccessNotification, BackOfficeUserManagerAuditer>();
|
||||
builder.AddNotificationHandler<UserLogoutSuccessNotification, BackOfficeUserManagerAuditer>();
|
||||
builder.AddNotificationHandler<UserLoginFailedNotification, BackOfficeUserManagerAuditer>();
|
||||
builder.AddNotificationHandler<UserForgotPasswordRequestedNotification, BackOfficeUserManagerAuditer>();
|
||||
builder.AddNotificationHandler<UserForgotPasswordChangedNotification, BackOfficeUserManagerAuditer>();
|
||||
builder.AddNotificationHandler<UserPasswordChangedNotification, BackOfficeUserManagerAuditer>();
|
||||
builder.AddNotificationHandler<UserPasswordResetNotification, BackOfficeUserManagerAuditer>();
|
||||
|
||||
return builder;
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user