Compare commits

..
Author SHA1 Message Date
Niels LyngsøandGitHub 246bb4c33d Merge branch 'main' into v17/feature/hook-api 2026-04-13 19:25:14 +02:00
Niels Lyngsø ac811c2624 hook package + controller + tests 2026-04-11 20:49:42 +02:00
2419 changed files with 20533 additions and 113352 deletions
-94
View File
@@ -1,94 +0,0 @@
---
name: umb-bump-version
description: Bump the Umbraco CMS version across all required files. Use when the user asks to bump, update, or set the version number — e.g., "bump version to 17.3.4", "set version to 18.0.0-rc", "update version". Accepts the target version as an argument.
argument-hint: <version> (e.g., 17.3.4, 18.0.0-rc)
---
# Bump Version - Umbraco CMS
Updates the Umbraco CMS version string across all files that track it.
**Do NOT use AskUserQuestion if a version argument is provided. Only ask if `$ARGUMENTS` is empty or cannot be parsed as a version.**
## Arguments
- `$ARGUMENTS` - Required: the target version string (e.g., `17.3.4`, `18.0.0-rc`)
## Files to Update
The following 5 files must be updated with the new version:
| # | File | Field |
|---|------|-------|
| 1 | `version.json` | `"version"` |
| 2 | `src/Umbraco.Web.UI.Client/package.json` | `"version"` |
| 3 | `src/Umbraco.Web.UI.Client/package-lock.json` | top-level `"version"` AND `packages[""].version` |
| 4 | `tests/Umbraco.Tests.AcceptanceTest/package.json` | `"version"` |
| 5 | `tests/Umbraco.Tests.AcceptanceTest/package-lock.json` | top-level `"version"` AND `packages[""].version` |
**Note**: For major version bumps (e.g., 17.x to 18.x), `src/Umbraco.Web.UI.Login/package.json` has a caret-ranged dependency on `@umbraco-cms/backoffice` (e.g., `^17.2.0`) that will need manual updating. This skill does not handle that — major bumps involve many other changes beyond version strings.
## Instructions
### 1. Parse and Validate the Version
Extract the version from `$ARGUMENTS`. It must be a valid semver-like string (e.g., `17.3.4`, `18.0.0-rc`, `17.4.0-preview.1`). If no version is provided or it cannot be parsed, ask the user for the target version.
### 2. Read the Current Version
Read `version.json` and extract the current `"version"` value. If the current version already equals the target version, report that the version is already set and stop — do not edit, stage, or commit anything.
Otherwise, display both versions:
```
Bumping version: {current} -> {target}
```
### 3. Update All Files
Update each of the 5 files listed above, replacing the old version with the new version. For each file:
- **`version.json`**: Replace the `"version"` value.
- **`package.json` files**: Replace the `"version"` value (near the top of the file).
- **`package-lock.json` files**: Replace BOTH the top-level `"version"` value AND the `"version"` inside the `"packages": { "": { ... } }` block. These are always in the first ~10 lines of the file.
Use targeted edits — do NOT rewrite entire files. Be precise to avoid changing version strings in dependency entries.
### 4. Verify
After all edits, grep for the target version value across the 5 files to confirm all updates landed correctly:
```bash
grep -n "\"version\": \"{version}\"" version.json src/Umbraco.Web.UI.Client/package.json src/Umbraco.Web.UI.Client/package-lock.json tests/Umbraco.Tests.AcceptanceTest/package.json tests/Umbraco.Tests.AcceptanceTest/package-lock.json
```
Expect exactly 7 matches (one per `package.json` and `version.json`, two per `package-lock.json`).
### 5. Stage and Commit
Stage only the 5 changed files:
```bash
git add version.json src/Umbraco.Web.UI.Client/package.json src/Umbraco.Web.UI.Client/package-lock.json tests/Umbraco.Tests.AcceptanceTest/package.json tests/Umbraco.Tests.AcceptanceTest/package-lock.json
```
Then commit with the message `Bump version to {version}.` — replacing `{version}` with the target version:
```bash
git commit -m "Bump version to {version}."
```
### 6. Report
Output a summary:
```
Version bumped to {version} in:
- version.json
- src/Umbraco.Web.UI.Client/package.json
- src/Umbraco.Web.UI.Client/package-lock.json
- tests/Umbraco.Tests.AcceptanceTest/package.json
- tests/Umbraco.Tests.AcceptanceTest/package-lock.json
Changes staged and committed.
```
-135
View File
@@ -1,135 +0,0 @@
---
name: umb-release-notes
description: Improve a set of auto-generated GitHub release notes for an Umbraco CMS release. Cross-checks the notes against every PR carrying the release label, adds any that are missing, re-files every PR under the most appropriate category, and strips purely-internal entries. Use whenever the user asks to tidy up, improve, complete, or recategorize release notes for a given version, or mentions a release-notes text file plus a version number.
argument-hint: <version> <path-to-generated-notes-file>
---
# Umbraco CMS - Improve Release Notes
Takes a file of auto-generated GitHub release notes and produces an improved version that:
1. **Is complete** — every merged PR carrying the `release/<version>` label appears.
2. **Is well-categorized** — every PR sits under the most appropriate heading.
3. **Is free of noise** — purely-internal entries of no value to a reader are removed.
The result is written to a **new** file alongside the input, so the user can diff the two.
**Run autonomously.** Do NOT use `AskUserQuestion` once the required arguments (version and input file path) are available — only ask if one of them is missing from `$ARGUMENTS` and cannot be inferred (see Arguments). Beyond that, make the categorization calls yourself using the rules below; if a handful are genuinely borderline, place them anyway and note the borderline ones in your closing summary so the user can override.
## Arguments
`$ARGUMENTS` contains two values:
1. **Version** — e.g. `17.5.0`, `18.1.0`. The GitHub label to search is `release/<version>` (so version `17.5.0` → label `release/17.5.0`).
2. **Input file path** — full path to the text file holding the auto-generated notes (e.g. `C:\Temp\release-17.5.0-rc.md`).
If either is missing, ask the user once for the missing value, then proceed.
## Prerequisites
Run `gh auth status`. If it fails, tell the user to authenticate `gh` (e.g. `gh auth login`) and stop — the skill needs the GitHub CLI to query PRs. The repo is always `umbraco/Umbraco-CMS`.
## Procedure
### 1. Read the input notes
Read the input file. Note its structure — it is GitHub's generated format:
- A leading HTML comment (`<!-- Release notes generated ... -->`).
- A `## What's Changed` heading followed by `### <emoji> <Category>` sub-headings, each with `* <title> by @<author> in <url>` bullets.
- A trailing `## New Contributors` section and a `**Full Changelog**: ...` line.
Extract the set of PR numbers already present (parse the `/pull/<number>` from each bullet). Preserve each existing bullet's **exact text** (title, author, URL) when you re-emit it — only its category placement may change.
### 2. Fetch every labelled PR
```bash
gh pr list --repo umbraco/Umbraco-CMS --label "release/<version>" --state closed --limit 1000 \
--json number,title,author,labels,mergedAt \
--jq '.[] | select(.mergedAt != null) | "\(.number)\t\(.author.login)\t\([.labels[].name] | join(", "))\t\(.title)"' | sort -n
```
This is the authoritative list of what the release *should* contain. Each row gives number, author, labels, title.
**Guard against silent truncation.** `gh pr list` caps at `--limit` without warning, so a large release could drop the overflow and the skill would still look "complete". Count the returned rows and compare against the limit:
```bash
gh pr list --repo umbraco/Umbraco-CMS --label "release/<version>" --state closed --limit 1000 --json number --jq 'length'
```
If this equals 1000, the limit was hit — raise `--limit` and re-fetch before continuing. Do **not** proceed on a truncated list.
### 3. Reconcile
- **Missing labelled PRs** (labelled but not in the input file): these must be **added**. Build a bullet as `* <title> by @<author> in https://github.com/umbraco/Umbraco-CMS/pull/<number>`.
- **Author handle.** `<author>` in the template is the raw `.author.login` value — the bullet supplies the leading `@`, so do not prepend another. `gh`'s `.author.login` already returns bot accounts with the `[bot]` suffix as part of the login — Dependabot comes back as `dependabot[bot]`, not `dependabot` or `app/dependabot` (the `app/` form only appears in git committer metadata and CODEOWNERS, never in `gh`'s JSON). So the login is already in the right shape; use it verbatim (e.g. `.author.login` of `dependabot[bot]` renders as `@dependabot[bot]`, matching what GitHub's generator wrote for the existing bullets). The only thing to guard against is accidentally stripping or altering the `[bot]` suffix.
- **PRs in the file but not labelled**: keep them. The generated notes span a commit range (see the `Full Changelog` compare link), so they legitimately include backports / earlier-version PRs that lack the current label. For any of these you need to categorize, fetch its labels with:
```bash
gh pr view <number> --repo umbraco/Umbraco-CMS --json number,title,labels \
--jq '"\(.number)\t\([.labels[].name] | join(", "))\t\(.title)"'
```
Do **not** invent or alter the `New Contributors` section — carry it over verbatim. You cannot reliably recompute first-time contributors, so leave it as the generator produced it (mention this in the summary).
### 4. Categorize every PR
Use exactly these headings, in this order. Omit any heading that ends up with no entries.
| Heading | What goes here | Primary signal |
|---|---|---|
| `### 🙌 Notable Changes` | **Don't recategorize existing entries.** Label-driven — but still add any missing PR carrying this label here. | label `category/notable` |
| `### 💥 Breaking Changes` | **Don't recategorize existing entries.** Label-driven — but still add any missing PR carrying this label here. | label `category/breaking` |
| `### 📦 Dependencies` | Dependency bumps | label `dependencies`; or dependabot author |
| `### 🚀 New Features` | New user- or developer-facing capability | label `type/feature` / `category/feature`; or title introduces/adds a genuinely new capability |
| `### 🚤 Performance` | Performance improvements | label `category/performance`; or `Performance:` title prefix |
| `### 🌈 Accessibility Improvements` | A11y improvements (labels, contrast, keyboard) | label `category/accessibility` / `accessibility`; or clear a11y intent (e.g. "improve contrast", "missing labels") |
| `### 🐛 Bug Fixes` | Fixes to broken/incorrect behaviour | default for anything describing a fix |
| `### 🧪 Testing` | Test additions/changes only | label `category/test-automation` / `area/test`; or `E2E`/`QA`/"acceptance tests"/"unit test coverage"/"add tests" titles |
| `### 🛡️ Code Quality, Documentation and Refactoring` | Refactors, deprecations, API tidy-ups, XML/MD documentation, knowledge-base (`MD`) updates | label `category/refactor`; or titles about refactoring, deprecating, renaming, documenting, constants extraction, MD/CLAUDE.md content |
| `### 🧑‍💻 Developer Experience` | Things that improve the experience of developers building on or contributing to Umbraco — dev tooling, build/watch ergonomics, test mocks/harnesses, backoffice dev utilities | `Developer Experience` title prefix; dev tooling; mock/harness changes |
**Rules:**
- **Notable and Breaking are off-limits for recategorization** — never move a PR that is *already in the input file* into or out of these sections; they are driven purely by their labels and the generator placed them correctly. This does **not** exempt them from completeness: a PR discovered as missing in step 3 that carries `category/notable` or `category/breaking` must still be **added** under the matching section.
- Label signals beat title wording, except a `Performance:`/`Developer Experience:` title prefix is decisive for its section.
- A PR with both `type/feature` and `category/refactor` whose title clearly describes a refactor (e.g. "swap relative imports", "re-export type") belongs under Code Quality, not New Features.
- "Add ... tests"/"unit test coverage" → Testing, even if it also touches docs. If a PR adds XML documentation *and* tests, lead with where the title's emphasis lies (documentation → Code Quality; test coverage → Testing).
- When a PR is genuinely 50/50, pick the more reader-useful heading and list it in your closing summary as borderline.
### 5. Remove purely-internal noise
Drop entries that have **no value to anyone reading release notes** — pure repository plumbing with no shipped impact. Examples:
- Branch/merge maintenance ("Fix main branch after merge issue").
- CI/pipeline fixes that don't change the product.
- Reverts of changes that never shipped in a release.
**Keep** anything that ships in the product or genuinely helps developers building on Umbraco — that includes documentation/MD updates, dev tooling, and test mocks (those go to Code Quality or Developer Experience, they are *not* noise). When unsure whether something is noise, keep it and flag it in the summary rather than silently dropping it. List every removal in your closing summary.
### 6. Write the output
Write to a new file in the **same folder** as the input, named by appending ` - with updates` before the extension:
- Input `C:\Temp\release-17.5.0-rc.md` → Output `C:\Temp\release-17.5.0-rc - with updates.md`
Preserve the leading HTML comment, the `## What's Changed` heading, the `## New Contributors` section, and the `**Full Changelog**` line exactly. Only the `### <category>` groupings and their bullets change.
### 7. Report
Give a concise summary:
- Count of PRs added (with their numbers), and which categories they landed in.
- Notable recategorizations (PRs moved out of the catch-all Bug Fixes into Features/Performance/Testing/etc.).
- Every entry removed, with the one-line reason.
- Any borderline calls the user may want to override.
- The output file path.
## Verification
Before reporting done, confirm:
- Every PR number from step 2 is present in the output (except any you deliberately removed in step 5 — and those must be in the removal list).
- No PR appears under more than one heading.
- Notable and Breaking sections are byte-for-byte unchanged from the input.
- The header comment, New Contributors, and Full Changelog lines are intact.
+1 -1
View File
@@ -23,7 +23,7 @@ jobs:
runs-on: ubuntu-latest
name: Build and Deploy Job
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v4
with:
submodules: true
- name: Build And Deploy
+1 -1
View File
@@ -26,7 +26,7 @@ jobs:
runs-on: ubuntu-latest
name: Build and Deploy Job
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v4
- name: Build And Deploy
id: builddeploy
uses: Azure/static-web-apps-deploy@v1
+6 -30
View File
@@ -1,52 +1,28 @@
name: Claude PR Review
on:
pull_request:
types: [opened, ready_for_review, reopened]
# NOTE: `pull_request_target` would let this workflow review fork PRs
# (with access to secrets), but the action currently fails during OIDC
# token exchange with "401 Unauthorized - Invalid OIDC token" on that
# event. PR #579 added `pull_request_target` routing to the action, but
# Anthropic's `/github-app-token-exchange` endpoint appears not to
# accept the token claims produced by that event. Re-enable once the
# upstream issue is resolved.
# See: https://github.com/anthropics/claude-code-action/issues/347
# https://github.com/anthropics/claude-code-action/issues/621
# pull_request_target:
# types: [opened, ready_for_review]
pull_request_target:
types: [opened, ready_for_review]
permissions:
contents: read
pull-requests: write
issues: write
id-token: write
actions: read
jobs:
review:
# Skip fork PRs: secrets are not exposed on `pull_request` events from
# forks, so the action would fail with a red check. Remove this clause
# once upstream fork support lands (tracked in
# https://github.com/anthropics/claude-code-action/issues/939) and we
# can re-enable the `pull_request_target` trigger above.
if: >-
github.event.pull_request.draft == false
&& github.event.pull_request.head.repo.full_name == github.repository
if: github.event.pull_request.draft == false
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v4
with:
fetch-depth: 1
- uses: anthropics/claude-code-action@v1
with:
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY_03 }}
# Enable progress tracking
track_progress: true
# Debug (set to true to show full output in logs, false to hide it and only post comments on the PR)
show_full_output: false
base_branch: "main"
additional_permissions: "actions: read"
claude_args: "--model claude-sonnet-4-6 --allowedTools 'Bash(gh:*),Bash(git:*)'"
prompt: |
+1 -1
View File
@@ -25,7 +25,7 @@ jobs:
(github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude')))
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v4
with:
fetch-depth: 1
+1 -1
View File
@@ -51,7 +51,7 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v6
uses: actions/checkout@v4
# We use the setup-dotnet action to set up .NET Core, otherwise the CodeQL CLI will not work with preview versions.
- name: Setup .NET from global.json
-84
View File
@@ -1,84 +0,0 @@
name: Issue Deduplication
on:
issues:
types: [ opened ]
workflow_dispatch:
inputs:
issue_number:
description: 'Issue number to analyze for duplicates'
required: true
type: number
jobs:
deduplicate:
runs-on: ubuntu-latest
timeout-minutes: 10
permissions:
contents: read
issues: write
id-token: write
steps:
- name: Checkout repository
uses: actions/checkout@v6
with:
fetch-depth: 1
- name: Check for duplicate issues
uses: anthropics/claude-code-action@v1
with:
prompt: |
Analyze this new issue and check if it's a duplicate of existing issues in the repository.
Issue: #${{ github.event.issue.number || inputs.issue_number }}
Repository: ${{ github.repository }}
Your task:
1. Use mcp__github__get_issue to get details of the current issue (#${{ github.event.issue.number || inputs.issue_number }})
2. Search for similar existing issues using mcp__github__search_issues with relevant keywords from the issue title and body
3. Compare the new issue with existing ones to identify potential duplicates
Criteria for duplicates:
- Same bug or error being reported
- Same feature request (even if worded differently)
- Same question being asked
- Issues describing the same root problem
If you find duplicates:
- Add a comment on the new issue linking to the original issue(s)
- Apply the "duplicate" and "state/needs-investigation" labels to the new issue
- Be polite and explain why it's a duplicate
- Suggest the user follow the original issue for updates
If it's NOT a duplicate:
- Don't add any comments
- You may apply appropriate topic labels based on the issue content
Use these tools:
- mcp__github__get_issue: Get issue details
- mcp__github__search_issues: Search for similar issues
- mcp__github__list_issues: List recent issues if needed
- mcp__github__add_issue_comment: Add a comment if duplicate found
- mcp__github__update_issue: Add labels
Be thorough but efficient. Focus on finding true duplicates, not just similar issues.
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY_03 }}
# Issues are opened by community members without write access, so the
# default OIDC token exchange fails with "User does not have write
# access on this repository". Pass `github_token` explicitly and set
# `allowed_non_write_users` to bypass that check. Safe here because
# `permissions:` and `--allowedTools` below are tightly scoped to
# issue operations only.
github_token: ${{ secrets.GITHUB_TOKEN }}
allowed_non_write_users: "*"
# Surface full SDK output (including tool calls and permission denials)
# to diagnose why Claude sometimes only partially completes (e.g. labels
# an issue but skips the comment). Safe to leave on — no secrets in output.
show_full_output: true
claude_args: |
--model claude-haiku-4-5 --allowedTools "mcp__github__get_issue,mcp__github__search_issues,mcp__github__list_issues,mcp__github__add_issue_comment,mcp__github__update_issue,mcp__github__get_issue_comments"
+2 -2
View File
@@ -34,7 +34,7 @@ jobs:
run:
working-directory: src/Umbraco.Web.UI.Client
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v4
- name: Use Node.js
uses: actions/setup-node@v4
with:
@@ -57,7 +57,7 @@ jobs:
run:
working-directory: src/Umbraco.Web.UI.Client
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v4
- name: Use Node.js
uses: actions/setup-node@v4
with:
+2 -5
View File
@@ -52,9 +52,7 @@ tools/docfx/
/build/csharp-docs/_site/
# Local config
.claude/*
!.claude/skills/
!.claude/settings.json
.claude/settings.local.json
.env.local
# Build
@@ -72,8 +70,7 @@ tools/docfx/
/src/Umbraco.Cms.StaticAssets/wwwroot/umbraco/assets
/src/Umbraco.Cms.StaticAssets/wwwroot/umbraco/js
/src/Umbraco.Cms.StaticAssets/wwwroot/umbraco/lib
/src/Umbraco.Cms.StaticAssets/wwwroot/umbraco/views/*
!/src/Umbraco.Cms.StaticAssets/wwwroot/umbraco/views/errors
/src/Umbraco.Cms.StaticAssets/wwwroot/umbraco/views
/src/Umbraco.Cms.StaticAssets/wwwroot/umbraco/login
# Environment specific data
+1 -49
View File
@@ -227,11 +227,9 @@ Project ownership is distributed across teams. Check individual project director
1. **Layered Architecture with Dependency Inversion**
- Core defines contracts (interfaces)
- Infrastructure implements contracts that need Infrastructure-owned machinery
- Infrastructure implements contracts
- Web/APIs consume implementations via DI
**Where service implementations live**: Services whose dependencies are satisfiable from Core interfaces alone (repositories, scope, config, other Core services) live in `Umbraco.Core/Services/` — this covers the majority of domain services (`MemberService`, `ContentService`, `MediaService`, `ContentTypeService`, `EntityService`, `AuditService`, `ExternalMemberService`, etc.). Service implementations only live in `Umbraco.Infrastructure/Services/Implement/` when they genuinely need Infrastructure concerns — Examine indexes (`ContentSearchService`, `MediaSearchService`, `IndexedEntitySearchService`), log files (`LogViewerRepository`), packaging internals (`PackagingService`), webhook firing (`WebhookFiringService`), distributed-job coordination (`DistributedJobService`). When adding a new service, default to Core and only move to Infrastructure if a concrete dependency forces it.
2. **Interface-First Design**
- All services defined as interfaces in Core
- Enables testing, polymorphism, extensibility
@@ -435,14 +433,6 @@ When a PR changes Management API controllers or models, the `OpenApi.json` file
The backoffice is published to npm as `@umbraco-cms/backoffice`. Runtime dependencies are provided via importmap; npm peerDependencies provide types only. For full details on dependency hoisting, version range logic, and plugin development, see `/src/Umbraco.Web.UI.Client/CLAUDE.md` → "npm Package Publishing".
### SQL Server 2100-parameter limit
Any `WHERE IN (@0, @1, ...)` built from a runtime-sized collection risks hitting SQL Server's 2100-parameter ceiling and throwing `SqlException` 8003 in production.
Batch with `IEnumerable<T>.InGroupsOf(Constants.Sql.MaxParameterCount)` or `Database.FetchByGroups(...)` whenever the collection size is driven by user data — not just when it currently fits. Watch for products of two scaling dimensions (documents × languages, properties × versions) and config-tunable batch sizes whose defaults are safe but ceilings aren't.
Full guidance, safe patterns and decision rule: see `/src/Umbraco.Infrastructure/CLAUDE.md` → "Avoiding the SQL Server 2100-parameter limit".
### Known Limitations
1. **Circular Dependencies**: Avoided via `Lazy<T>` or event notifications
@@ -513,42 +503,6 @@ Labels are only added, never removed. Claude applies only labels it is confident
---
## 8. Code Comment Policy
**Default to no comment.** Applies to all code in this repository — C#, TypeScript, Razor, build scripts. Well-named identifiers and small functions are the primary form of self-documentation; comments are a fallback for the rare cases where the code itself cannot carry the meaning.
### When NOT to comment
- **Don't restate what the code does.** A line calling `resetState()` does not need `// Reset state`. A method named `validateInput` does not need `// Validate input`.
- **Don't narrate a sequence of calls.** If three lines run in order, the order is in the code — don't paraphrase it above.
- **Don't reference the current task, fix, callers, or PR.** No `// Fix for X`, `// Used by Y`, `// Added for the Z flow`, `// See PR #1234`. That belongs in commit messages and PR descriptions; in source it rots as the codebase evolves.
### When a comment IS justified
Write a comment only when **removing it would leave a future reader confused**. Concretely:
- **A non-obvious WHY.** A hidden constraint, business rule, or ordering requirement that is not visible from the code.
- **A workaround for a specific bug or platform quirk.** Link the issue (`(#21996)`, `https://...`) so the comment can be deleted once the upstream fix lands.
- **A subtle invariant** that the type system or method names do not enforce.
- **An edge case the code intentionally handles** that would surprise a reader (e.g. "must run before X because Y").
- **API documentation** — XML doc comments on C# members, JSDoc on exported TypeScript symbols. Required for the public contract; still keep them concise.
### TODOs
Allowed, but cheap to write and cheaper to leave behind. Keep them short and trackable: `// TODO (V19): remove once obsolete overload is gone` or `// TODO: pagination [NL]`. A TODO should have an author or a version trigger.
---
## 9. Testing Practices
### Tests for a bug fix must fail before the fix
Verify any test you add for a bug fix actually catches the bug: either write the failing test first (TDD), or temporarily revert the production change and confirm the test fails before re-applying. A test that passes both ways proves nothing. Watch for coincidental passes — default seed/sort orders can make a buggy path produce the right answer for the test's specific inputs; construct inputs so the broken and fixed behaviours give visibly different results.
For integration tests that exercise caching or cache refreshers, see `tests/Umbraco.Tests.Integration/CLAUDE.md` — the harness disables caching by default, which can produce false greens.
---
## Quick Reference
### Essential Commands
@@ -607,8 +561,6 @@ For detailed information about individual projects, see their CLAUDE.md files:
- **API Infrastructure**: `/src/Umbraco.Cms.Api.Common/CLAUDE.md` - OpenAPI, authentication, serialization
- **Backoffice Frontend**: `/src/Umbraco.Web.UI.Client/CLAUDE.md` - Lit web components, extension system, auth client
**Important**: When working on backoffice client code (anything under `src/Umbraco.Web.UI.Client/`), read `/src/Umbraco.Web.UI.Client/CLAUDE.md` first. It contains action-specific checklists (deprecation, testing, security, etc.) that are not duplicated here.
### Getting Help
- **Official Docs**: https://docs.umbraco.com/
+30 -33
View File
@@ -13,55 +13,55 @@
</ItemGroup>
<!-- Microsoft packages -->
<ItemGroup>
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation" Version="10.0.6" />
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation" Version="10.0.4" />
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="4.14.0" />
<PackageVersion Include="Microsoft.Data.Sqlite" Version="10.0.6" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="10.0.6" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.SqlServer" Version="10.0.6" />
<PackageVersion Include="Microsoft.Extensions.Caching.Abstractions" Version="10.0.6" />
<PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="10.0.6" />
<PackageVersion Include="Microsoft.Extensions.Configuration.Abstractions" Version="10.0.6" />
<PackageVersion Include="Microsoft.Extensions.Configuration.Json" Version="10.0.6" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="10.0.6" />
<PackageVersion Include="Microsoft.Extensions.FileProviders.Embedded" Version="10.0.6" />
<PackageVersion Include="Microsoft.Extensions.FileProviders.Physical" Version="10.0.6" />
<PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="10.0.6" />
<PackageVersion Include="Microsoft.Extensions.Http" Version="10.0.6" />
<PackageVersion Include="Microsoft.Extensions.Identity.Core" Version="10.0.6" />
<PackageVersion Include="Microsoft.Extensions.Identity.Stores" Version="10.0.6" />
<PackageVersion Include="Microsoft.Extensions.Logging" Version="10.0.6" />
<PackageVersion Include="Microsoft.Extensions.Options" Version="10.0.6" />
<PackageVersion Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="10.0.6" />
<PackageVersion Include="Microsoft.Extensions.Options.DataAnnotations" Version="10.0.6" />
<PackageVersion Include="Microsoft.Extensions.Caching.Hybrid" Version="10.5.0" />
<PackageVersion Include="Microsoft.Data.Sqlite" Version="10.0.4" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="10.0.4" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.SqlServer" Version="10.0.4" />
<PackageVersion Include="Microsoft.Extensions.Caching.Abstractions" Version="10.0.4" />
<PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="10.0.4" />
<PackageVersion Include="Microsoft.Extensions.Configuration.Abstractions" Version="10.0.4" />
<PackageVersion Include="Microsoft.Extensions.Configuration.Json" Version="10.0.4" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="10.0.4" />
<PackageVersion Include="Microsoft.Extensions.FileProviders.Embedded" Version="10.0.4" />
<PackageVersion Include="Microsoft.Extensions.FileProviders.Physical" Version="10.0.4" />
<PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="10.0.4" />
<PackageVersion Include="Microsoft.Extensions.Http" Version="10.0.4" />
<PackageVersion Include="Microsoft.Extensions.Identity.Core" Version="10.0.4" />
<PackageVersion Include="Microsoft.Extensions.Identity.Stores" Version="10.0.4" />
<PackageVersion Include="Microsoft.Extensions.Logging" Version="10.0.4" />
<PackageVersion Include="Microsoft.Extensions.Options" Version="10.0.4" />
<PackageVersion Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="10.0.4" />
<PackageVersion Include="Microsoft.Extensions.Options.DataAnnotations" Version="10.0.4" />
<PackageVersion Include="Microsoft.Extensions.Caching.Hybrid" Version="10.4.0" />
<PackageVersion Include="System.Linq.Async" Version="7.0.0" />
</ItemGroup>
<!-- Umbraco packages -->
<ItemGroup>
<PackageVersion Include="Umbraco.JsonSchema.Extensions" Version="0.4.0" />
<PackageVersion Include="Umbraco.JsonSchema.Extensions" Version="0.3.0" />
</ItemGroup>
<!-- Third-party packages -->
<ItemGroup>
<PackageVersion Include="Asp.Versioning.Mvc" Version="8.1.1" />
<PackageVersion Include="Asp.Versioning.Mvc.ApiExplorer" Version="8.1.1" />
<PackageVersion Include="Dazinator.Extensions.FileProviders" Version="2.0.0" />
<PackageVersion Include="Examine" Version="3.8.0" />
<PackageVersion Include="Examine.Core" Version="3.8.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="JsonPatch.Net" Version="3.3.0" />
<PackageVersion Include="K4os.Compression.LZ4" Version="1.3.8" />
<PackageVersion Include="MailKit" Version="4.16.0" />
<PackageVersion Include="MailKit" Version="4.15.1" />
<PackageVersion Include="Markdig" Version="0.45.0" />
<PackageVersion Include="Markdown" Version="2.2.1" />
<PackageVersion Include="MessagePack" Version="3.1.7" />
<PackageVersion Include="MessagePack" Version="3.1.4" />
<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.2.0" />
<PackageVersion Include="NPoco.SqlServer" Version="6.2.0" />
<PackageVersion Include="OpenIddict.Abstractions" Version="7.4.0" />
<PackageVersion Include="OpenIddict.AspNetCore" Version="7.4.0" />
<PackageVersion Include="OpenIddict.EntityFrameworkCore" Version="7.4.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.1" />
<PackageVersion Include="Serilog.AspNetCore" Version="9.0.0" />
<PackageVersion Include="Serilog.Enrichers.Process" Version="3.0.0" />
@@ -77,7 +77,7 @@
<PackageVersion Include="SixLabors.ImageSharp" Version="3.1.12" />
<PackageVersion Include="SixLabors.ImageSharp.Web" Version="3.2.0" />
<!-- When updating this version, also update templates/UmbracoExtension/Umbraco.Extension.csproj -->
<PackageVersion Include="Swashbuckle.AspNetCore" Version="10.1.7" />
<PackageVersion Include="Swashbuckle.AspNetCore" Version="10.1.4" />
</ItemGroup>
<!-- Transitive pinned versions (only required because our direct dependencies have vulnerable versions of transitive dependencies) -->
<ItemGroup>
@@ -88,8 +88,5 @@
<!-- Markdown references vulnerable version of the following: -->
<!-- TODO (V19): Remove these pinned dependencies when the Markdown dependency is removed. -->
<PackageVersion Include="System.Text.RegularExpressions" Version="4.3.1" />
<!-- Examine (via Microsoft.AspNetCore.DataProtection 8.0.4) references a vulnerable version of the following: -->
<!-- TODO: Remove this pinned dependency when Examine updates its Microsoft.AspNetCore.DataProtection reference. -->
<PackageVersion Include="System.Security.Cryptography.Xml" Version="10.0.6" />
</ItemGroup>
</Project>
</Project>
+113 -61
View File
@@ -188,9 +188,16 @@ stages:
parameters:
nodeVersion: ${{ variables.nodeVersion }}
npm_config_cache: ${{ variables.npm_config_cache }}
- template: templates/set-npm-version.yml
parameters:
workingDirectory: tests/Umbraco.Tests.AcceptanceTest
- bash: |
echo "##[command]Install nbgv"
dotnet tool install --tool-path . nbgv
echo "##[command]Running nbgv get-version"
PACKAGE_VERSION=$(nbgv get-version -v NpmPackageVersion)
echo "##[command]Running npm version"
echo "##[debug]Version: $PACKAGE_VERSION"
cd tests/Umbraco.Tests.AcceptanceTest
npm version $PACKAGE_VERSION --allow-same-version --no-git-tag-version
displayName: Set NPM Version
- bash: |
echo "##[command]Running npm pack"
mkdir $(Build.ArtifactStagingDirectory)/npm-testhelpers
@@ -825,56 +832,84 @@ stages:
publishFeedCredentials: "MyGet - Umbraco Nightly"
${{ else }}:
publishFeedCredentials: "MyGet - Pre-releases"
# Pre-release/nightly feeds: keep the `latest` dist-tag default (no `next` split).
- job:
displayName: Push to pre-release feed (npm)
steps:
- template: templates/npm-publish.yml
parameters:
artifactName: npm
- checkout: none
- download: current
artifact: npm
- bash: |
# Check if we are on a nightly build
if [ $isNightly = "False" ]; then
echo "##[debug]Prerelease build detected"
registry="https://www.myget.org/F/umbracoprereleases/npm/"
else
echo "##[debug]Nightly build detected"
registry="https://www.myget.org/F/umbraconightly/npm/"
fi
echo "@umbraco-cms:registry=$registry" >> .npmrc
env:
isNightly: ${{parameters.isNightly}}
workingDirectory: $(Pipeline.Workspace)/npm
displayName: Add scoped registry to .npmrc
- task: npmAuthenticate@0
displayName: Authenticate with npm (MyGet)
inputs:
workingFile: "$(Pipeline.Workspace)/npm/.npmrc"
customEndpoint: "MyGet (npm) - Umbracoprereleases, MyGet (npm) - Umbraconightly"
displayName: Push to npm (MyGet)
${{ if eq(parameters.isNightly, true) }}:
registry: https://www.myget.org/F/umbraconightly/npm/
${{ else }}:
registry: https://www.myget.org/F/umbracoprereleases/npm/
- bash: |
# Setup temp npm project to load in defaults from the local .npmrc
npm init -y
# Find the first .tgz file in the current directory and publish it
files=( ./*.tgz )
npm publish "${files[0]}"
displayName: Push to npm (MyGet)
workingDirectory: $(Pipeline.Workspace)/npm
- job: PublishTestHelpersNpm
displayName: Push TestHelpers to pre-release feed (npm)
steps:
- template: templates/npm-publish.yml
parameters:
artifactName: npm-testhelpers
- checkout: none
- download: current
artifact: npm-testhelpers
- bash: |
# Check if we are on a nightly build
if [ $isNightly = "False" ]; then
echo "##[debug]Prerelease build detected"
registry="https://www.myget.org/F/umbracoprereleases/npm/"
else
echo "##[debug]Nightly build detected"
registry="https://www.myget.org/F/umbraconightly/npm/"
fi
echo "@umbraco-cms:registry=$registry" >> .npmrc
env:
isNightly: ${{parameters.isNightly}}
workingDirectory: $(Pipeline.Workspace)/npm-testhelpers
displayName: Add scoped registry to .npmrc
- task: npmAuthenticate@0
displayName: Authenticate with npm (MyGet)
inputs:
workingFile: "$(Pipeline.Workspace)/npm-testhelpers/.npmrc"
customEndpoint: "MyGet (npm) - Umbracoprereleases, MyGet (npm) - Umbraconightly"
displayName: Push test helpers to npm (MyGet)
${{ if eq(parameters.isNightly, true) }}:
registry: https://www.myget.org/F/umbraconightly/npm/
${{ else }}:
registry: https://www.myget.org/F/umbracoprereleases/npm/
- bash: |
# Setup temp npm project to load in defaults from the local .npmrc
npm init -y
# Find the first .tgz file in the current directory and publish it
files=( ./*.tgz )
npm publish "${files[0]}"
displayName: Push test helpers to npm (MyGet)
workingDirectory: $(Pipeline.Workspace)/npm-testhelpers
- stage: Deploy_NuGet
displayName: NuGet release
dependsOn: Deploy_MyGet
# Run only when Deploy_MyGet actually ran (succeeded or failed) — not when it was skipped due to an upstream test failure.
# Inspect Deploy_MyGet's direct result rather than succeeded()/failed(), which are transitive across the full ancestor graph.
# Approval is required every run via the WaitForApproval job below.
condition: and(in(dependencies.Deploy_MyGet.result, 'Succeeded', 'SucceededWithIssues', 'Failed'), or(eq(dependencies.Build.outputs['A.build.NBGV_PublicRelease'], 'True'), ${{parameters.nuGetDeploy}}))
condition: and(succeeded(), or(eq(dependencies.Build.outputs['A.build.NBGV_PublicRelease'], 'True'), ${{parameters.nuGetDeploy}}))
jobs:
- job: WaitForApproval
displayName: Wait for manual approval
pool: server
timeoutInMinutes: 4320 # 3 days
steps:
- task: ManualValidation@0
displayName: Manual approval to push to NuGet
inputs:
notifyUsers: ''
instructions: 'Approve to push the NuGet release.'
onTimeout: 'reject'
- job: Push
displayName: Push to NuGet
dependsOn: WaitForApproval
- job:
pool:
vmImage: "windows-latest" # NuGetCommand@2 is no longer supported on Ubuntu 24.04 so we'll use windows until an alternative is available.
displayName: Push to NuGet
steps:
- checkout: none
- task: DownloadPipelineArtifact@2
@@ -892,36 +927,56 @@ stages:
- stage: Deploy_Npm
displayName: Npm release
# Inspect Deploy_NuGet.result directly so a MyGet failure (which is in the transitive ancestor graph)
# doesn't cascade-skip this stage via succeeded(). Deploy_NuGet must itself have succeeded — a NuGet
# failure deliberately blocks the npm release.
condition: and(in(dependencies.Deploy_NuGet.result, 'Succeeded', 'SucceededWithIssues'), or(eq(dependencies.Build.outputs['A.build.NBGV_PublicRelease'], 'True'), ${{parameters.npmDeploy}}))
condition: and(succeeded(), or(eq(dependencies.Build.outputs['A.build.NBGV_PublicRelease'], 'True'), ${{parameters.npmDeploy}}))
dependsOn:
- Deploy_NuGet
variables:
# `latest` for stable releases, `next` for prereleases.
npmDistTag: $[ iif(eq(stageDependencies.Build.A.outputs['build.NBGV_PrereleaseVersionNoLeadingHyphen'], ''), 'latest', 'next') ]
jobs:
- job: Publish
displayName: Push to NPM
steps:
- template: templates/npm-publish.yml
parameters:
artifactName: npm
registry: https://registry.npmjs.org/
- checkout: none
- download: current
artifact: npm
- bash: echo "@umbraco-cms:registry=https://registry.npmjs.org" >> .npmrc
workingDirectory: $(Pipeline.Workspace)/npm
displayName: Add scoped registry to .npmrc
- task: npmAuthenticate@0
displayName: Authenticate with npm
inputs:
workingFile: $(Pipeline.Workspace)/npm/.npmrc
customEndpoint: "NPM - Umbraco Backoffice"
displayName: Push to npm
npmTag: $(npmDistTag)
- script: |
# Setup temp npm project to load in defaults from the local .npmrc
npm init -y
# Find the first .tgz file in the current directory and publish it
files=( ./*.tgz )
npm publish "${files[0]}"
displayName: Push to npm
workingDirectory: $(Pipeline.Workspace)/npm
- job: PublishTestHelpers
displayName: Push Test Helpers to NPM
steps:
- template: templates/npm-publish.yml
parameters:
artifactName: npm-testhelpers
registry: https://registry.npmjs.org/
- checkout: none
- download: current
artifact: npm-testhelpers
- bash: echo "@umbraco-cms:registry=https://registry.npmjs.org" >> .npmrc
workingDirectory: $(Pipeline.Workspace)/npm-testhelpers
displayName: Add scoped registry to .npmrc
- task: npmAuthenticate@0
displayName: Authenticate with npm
inputs:
workingFile: $(Pipeline.Workspace)/npm-testhelpers/.npmrc
customEndpoint: "NPM - Umbraco Backoffice"
displayName: Push test helpers to npm
npmTag: $(npmDistTag)
- script: |
# Setup temp npm project to load in defaults from the local .npmrc
npm init -y
# Find the first .tgz file in the current directory and publish it
files=( ./*.tgz )
npm publish "${files[0]}"
displayName: Push test helpers to npm
workingDirectory: $(Pipeline.Workspace)/npm-testhelpers
- stage: Upload_API_Docs
pool:
@@ -933,10 +988,7 @@ stages:
- Build
- Build_Docs
- Deploy_NuGet
# Build_Docs must have produced artifacts (we won't upload anything otherwise) and Deploy_NuGet must
# have succeeded — a NuGet failure deliberately blocks the docs upload. Direct result checks avoid
# transitive succeeded()/failed() which would cascade-skip on a MyGet failure.
condition: and(in(dependencies.Build_Docs.result, 'Succeeded', 'SucceededWithIssues'), in(dependencies.Deploy_NuGet.result, 'Succeeded', 'SucceededWithIssues'), or(eq(dependencies.Build.outputs['A.build.NBGV_PublicRelease'], 'True'), ${{parameters.uploadApiDocs}}))
condition: and(succeeded(), or(eq(dependencies.Build.outputs['A.build.NBGV_PublicRelease'], 'True'), ${{parameters.uploadApiDocs}}))
jobs:
- job:
displayName: Upload C# Docs
+7 -7
View File
@@ -5,10 +5,10 @@ trigger: none
schedules:
- cron: '0 3 * * *'
displayName: Daily 3AM build (v17/dev)
displayName: Daily 3AM build (main)
branches:
include:
- v17/dev
- main
parameters:
- name: skipIntegrationTests
@@ -117,7 +117,7 @@ stages:
- stage: Integration
displayName: Integration Tests
dependsOn: Build
condition: and(succeeded(), ${{ eq(parameters.skipIntegrationTests, false) }})
condition: ${{ eq(parameters.skipIntegrationTests, false) }}
jobs:
# Integration Tests (SQLite)
- job:
@@ -319,8 +319,8 @@ stages:
- stage: DefaultConfigE2E
displayName: Default Config E2E Tests
dependsOn: [Build, Integration]
condition: in(dependencies.Build.result, 'Succeeded', 'SucceededWithIssues')
dependsOn: Integration
condition: always()
variables:
npm_config_cache: $(Pipeline.Workspace)/.npm_e2e
# Enable console logging in Release mode
@@ -500,8 +500,8 @@ stages:
- stage: AdditionalConfigE2E
displayName: Additional Config E2E Tests
dependsOn: [Build, DefaultConfigE2E]
condition: in(dependencies.Build.result, 'Succeeded', 'SucceededWithIssues')
dependsOn: DefaultConfigE2E
condition: always()
variables:
npm_config_cache: $(Pipeline.Workspace)/.npm_e2e
ASPNETCORE_URLS: https://localhost:44331
+10 -3
View File
@@ -6,9 +6,16 @@ steps:
versionSource: 'fromFile'
versionFilePath: src/Umbraco.Web.UI.Client/.nvmrc
- template: set-npm-version.yml
parameters:
workingDirectory: src/Umbraco.Web.UI.Client
- bash: |
echo "##[command]Install nbgv"
dotnet tool install --tool-path . nbgv
echo "##[command]Running nbgv get-version"
PACKAGE_VERSION=$(nbgv get-version -v NpmPackageVersion)
echo "##[command]Running npm version"
echo "##[debug]Version: $PACKAGE_VERSION"
cd src/Umbraco.Web.UI.Client
npm version $PACKAGE_VERSION --allow-same-version --no-git-tag-version
displayName: Set NPM Version
- task: Cache@2
displayName: Cache node_modules
-28
View File
@@ -1,28 +0,0 @@
parameters:
- name: artifactName # "npm" or "npm-testhelpers"
type: string
- name: registry # scoped-registry URL to publish to
type: string
- name: customEndpoint # npmAuthenticate service connection(s)
type: string
- name: displayName # label for the publish step
type: string
- name: npmTag # dist-tag to publish under
type: string
default: latest
steps:
- checkout: none
- download: current
artifact: ${{ parameters.artifactName }}
- script: npm config set @umbraco-cms:registry ${{ parameters.registry }} --location=project
displayName: Add scoped registry to .npmrc
workingDirectory: $(Pipeline.Workspace)/${{ parameters.artifactName }}
- task: npmAuthenticate@0
displayName: Authenticate with npm
inputs:
workingFile: $(Pipeline.Workspace)/${{ parameters.artifactName }}/.npmrc
customEndpoint: ${{ parameters.customEndpoint }}
- script: npm publish *.tgz --tag ${{ parameters.npmTag }}
displayName: ${{ parameters.displayName }}
workingDirectory: $(Pipeline.Workspace)/${{ parameters.artifactName }}
-15
View File
@@ -1,15 +0,0 @@
parameters:
- name: workingDirectory
type: string
steps:
- bash: |
echo "##[command]Install nbgv"
dotnet tool install --tool-path . nbgv
echo "##[command]Running nbgv get-version"
PACKAGE_VERSION=$(nbgv get-version -v NpmPackageVersion)
echo "##[command]Running npm version"
echo "##[debug]Version: $PACKAGE_VERSION"
cd ${{ parameters.workingDirectory }}
npm version $PACKAGE_VERSION --allow-same-version --no-git-tag-version
displayName: Set NPM Version
@@ -1,47 +0,0 @@
using Microsoft.AspNetCore.Http;
using Umbraco.Cms.Core.DeliveryApi;
using Umbraco.Cms.Core.Models.PublishedContent;
namespace Umbraco.Cms.Api.Delivery.Caching;
/// <summary>
/// Default implementation of <see cref="IDeliveryApiOutputCacheRequestFilter"/> that prevents caching
/// for preview mode requests and requests without public access.
/// </summary>
public class DefaultDeliveryApiOutputCacheRequestFilter : IDeliveryApiOutputCacheRequestFilter
{
private readonly IRequestPreviewService _requestPreviewService;
private readonly IApiAccessService _apiAccessService;
/// <summary>
/// Initializes a new instance of the <see cref="DefaultDeliveryApiOutputCacheRequestFilter"/> class.
/// </summary>
/// <param name="requestPreviewService">The preview service.</param>
/// <param name="apiAccessService">The API access service.</param>
public DefaultDeliveryApiOutputCacheRequestFilter(IRequestPreviewService requestPreviewService, IApiAccessService apiAccessService)
{
_requestPreviewService = requestPreviewService;
_apiAccessService = apiAccessService;
}
/// <inheritdoc />
public virtual bool IsCacheable(HttpContext context)
=> IsPreview() is false && HasPublicAccess();
/// <inheritdoc />
public virtual bool IsCacheable(HttpContext context, IPublishedContent content) => true;
/// <summary>
/// Returns <c>true</c> if the current request is a preview request; <c>false</c> if the request
/// is not a preview and may be cached.
/// </summary>
protected virtual bool IsPreview()
=> _requestPreviewService.IsPreview();
/// <summary>
/// Returns <c>true</c> if the current request has public access; <c>false</c> if the request
/// is not publicly accessible and should not be cached.
/// </summary>
protected virtual bool HasPublicAccess()
=> _apiAccessService.HasPublicAccess();
}
@@ -1,17 +0,0 @@
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Cache;
using Umbraco.Cms.Core.Models.PublishedContent;
namespace Umbraco.Cms.Api.Delivery.Caching;
/// <summary>
/// Tags cached pages for delivery API output caching with their content type alias, enabling eviction by content type.
/// </summary>
internal sealed class DeliveryApiContentTypeOutputCacheTagProvider : IDeliveryApiOutputCacheTagProvider
{
/// <inheritdoc />
public IEnumerable<string> GetTags(IPublishedContent content)
{
yield return Constants.DeliveryApi.OutputCache.ContentTypeTagPrefix + content.ContentType.Alias;
}
}
@@ -1,135 +0,0 @@
using Microsoft.AspNetCore.OutputCaching;
using Microsoft.Extensions.Logging;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Cache;
using Umbraco.Cms.Core.Events;
using Umbraco.Cms.Core.Notifications;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Services.Changes;
using Umbraco.Cms.Core.Sync;
using Umbraco.Cms.Web.Common.Caching;
namespace Umbraco.Cms.Api.Delivery.Caching;
/// <summary>
/// Handles <see cref="ContentCacheRefresherNotification"/> to evict Delivery API output cache entries
/// when content is published, unpublished, moved, or deleted. Also evicts responses for content
/// that references the changed content via picker properties (umbDocument relations).
/// </summary>
internal sealed class DeliveryApiDocumentOutputCacheEvictionHandler
: RelationOutputCacheEvictionHandlerBase, INotificationAsyncHandler<ContentCacheRefresherNotification>
{
private readonly IEnumerable<IDeliveryApiOutputCacheEvictionProvider> _evictionProviders;
private readonly ILogger<DeliveryApiDocumentOutputCacheEvictionHandler> _logger;
/// <summary>
/// Initializes a new instance of the <see cref="DeliveryApiDocumentOutputCacheEvictionHandler"/> class.
/// </summary>
/// <param name="outputCacheStore">The output cache store for evicting cached responses.</param>
/// <param name="relationService">The relation service for querying entity references.</param>
/// <param name="idKeyMap">The ID/key mapping service for converting between integer IDs and GUIDs.</param>
/// <param name="evictionProviders">Custom eviction providers for additional tag-based eviction.</param>
/// <param name="logger">The logger.</param>
public DeliveryApiDocumentOutputCacheEvictionHandler(
IOutputCacheStore outputCacheStore,
IRelationService relationService,
IIdKeyMap idKeyMap,
IEnumerable<IDeliveryApiOutputCacheEvictionProvider> evictionProviders,
ILogger<DeliveryApiDocumentOutputCacheEvictionHandler> logger)
: base(outputCacheStore, relationService, idKeyMap)
{
_evictionProviders = evictionProviders;
_logger = logger;
}
/// <inheritdoc />
public async Task HandleAsync(ContentCacheRefresherNotification notification, CancellationToken cancellationToken)
{
if (notification.MessageType != MessageType.RefreshByPayload
|| notification.MessageObject is not ContentCacheRefresher.JsonPayload[] payloads)
{
return;
}
var changedEntityIds = new List<int>();
foreach (ContentCacheRefresher.JsonPayload payload in payloads)
{
if (payload.Blueprint)
{
continue;
}
await EvictForPayloadAsync(payload, cancellationToken);
changedEntityIds.Add(payload.Id);
}
// Evict content that references the changed content via picker properties.
await EvictRelatedContentAsync(
changedEntityIds,
Constants.Conventions.RelationTypes.RelatedDocumentAlias,
Constants.DeliveryApi.OutputCache.ContentTagPrefix,
_logger,
cancellationToken);
}
private async Task EvictForPayloadAsync(ContentCacheRefresher.JsonPayload payload, CancellationToken cancellationToken)
{
if (payload.ChangeTypes.HasFlag(TreeChangeTypes.RefreshAll))
{
// Evict all Delivery API responses — media responses may reference content via picker properties.
_logger.LogDebug("Content refresh all — evicting all Delivery API output cache entries.");
await OutputCacheStore.EvictByTagAsync(Constants.DeliveryApi.OutputCache.AllTag, cancellationToken);
return;
}
if (payload.Key.HasValue is false)
{
return;
}
Guid contentKey = payload.Key.Value;
if (_logger.IsEnabled(LogLevel.Debug))
{
_logger.LogDebug("Evicting Delivery API output cache for content {ContentKey}.", contentKey);
}
await OutputCacheStore.EvictByTagAsync(Constants.DeliveryApi.OutputCache.ContentTagPrefix + contentKey, cancellationToken);
if (payload.ChangeTypes.HasFlag(TreeChangeTypes.RefreshBranch))
{
if (_logger.IsEnabled(LogLevel.Debug))
{
_logger.LogDebug("Evicting Delivery API output cache for descendants of {ContentKey}.", contentKey);
}
await OutputCacheStore.EvictByTagAsync(Constants.DeliveryApi.OutputCache.AncestorTagPrefix + contentKey, cancellationToken);
}
await InvokeCustomEvictionProvidersAsync(payload, contentKey, cancellationToken);
}
private async Task InvokeCustomEvictionProvidersAsync(ContentCacheRefresher.JsonPayload payload, Guid contentKey, CancellationToken cancellationToken)
{
var context = new OutputCacheContentChangedContext(
payload.Id,
contentKey,
payload.PublishedCultures ?? [],
payload.UnpublishedCultures ?? []);
foreach (IDeliveryApiOutputCacheEvictionProvider provider in _evictionProviders)
{
IEnumerable<string> additionalTags = await provider.GetAdditionalEvictionTagsAsync(context, cancellationToken);
foreach (var tag in additionalTags)
{
if (_logger.IsEnabled(LogLevel.Debug))
{
_logger.LogDebug("Evicting Delivery API output cache tag {Tag} via custom provider.", tag);
}
await OutputCacheStore.EvictByTagAsync(tag, cancellationToken);
}
}
}
}
@@ -1,80 +0,0 @@
using Microsoft.AspNetCore.OutputCaching;
using Microsoft.Extensions.Logging;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Cache;
using Umbraco.Cms.Core.Events;
using Umbraco.Cms.Core.Notifications;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Services.Changes;
using Umbraco.Cms.Core.Sync;
using Umbraco.Cms.Web.Common.Caching;
namespace Umbraco.Cms.Api.Delivery.Caching;
/// <summary>
/// Handles <see cref="MediaCacheRefresherNotification"/> to evict Delivery API output cache entries
/// when media is created, updated, or deleted. Also evicts content responses that reference
/// the changed media via picker properties (umbMedia relations).
/// </summary>
internal sealed class DeliveryApiMediaOutputCacheEvictionHandler
: RelationOutputCacheEvictionHandlerBase, INotificationAsyncHandler<MediaCacheRefresherNotification>
{
private readonly ILogger<DeliveryApiMediaOutputCacheEvictionHandler> _logger;
/// <summary>
/// Initializes a new instance of the <see cref="DeliveryApiMediaOutputCacheEvictionHandler"/> class.
/// </summary>
/// <param name="outputCacheStore">The output cache store for evicting cached responses.</param>
/// <param name="relationService">The relation service for querying entity references.</param>
/// <param name="idKeyMap">The ID/key mapping service for converting between integer IDs and GUIDs.</param>
/// <param name="logger">The logger.</param>
public DeliveryApiMediaOutputCacheEvictionHandler(
IOutputCacheStore outputCacheStore,
IRelationService relationService,
IIdKeyMap idKeyMap,
ILogger<DeliveryApiMediaOutputCacheEvictionHandler> logger)
: base(outputCacheStore, relationService, idKeyMap)
=> _logger = logger;
/// <inheritdoc />
public async Task HandleAsync(MediaCacheRefresherNotification notification, CancellationToken cancellationToken)
{
if (notification.MessageType != MessageType.RefreshByPayload
|| notification.MessageObject is not MediaCacheRefresher.JsonPayload[] payloads)
{
return;
}
foreach (MediaCacheRefresher.JsonPayload payload in payloads)
{
if (payload.ChangeTypes.HasFlag(TreeChangeTypes.RefreshAll))
{
// Evict all Delivery API responses — content responses may include referenced media,
// so evicting only media entries would leave stale media references in content responses.
_logger.LogDebug("Media refresh all — evicting all Delivery API output cache entries.");
await OutputCacheStore.EvictByTagAsync(Constants.DeliveryApi.OutputCache.AllTag, cancellationToken);
return;
}
if (payload.Key.HasValue is false)
{
continue;
}
if (_logger.IsEnabled(LogLevel.Debug))
{
_logger.LogDebug("Evicting Delivery API output cache for media {MediaKey}.", payload.Key.Value);
}
await OutputCacheStore.EvictByTagAsync(Constants.DeliveryApi.OutputCache.MediaTagPrefix + payload.Key.Value, cancellationToken);
}
// Evict content that references the changed media via picker properties.
await EvictRelatedContentAsync(
payloads.Select(p => p.Id),
Constants.Conventions.RelationTypes.RelatedMediaAlias,
Constants.DeliveryApi.OutputCache.ContentTagPrefix,
_logger,
cancellationToken);
}
}
@@ -1,54 +0,0 @@
using Microsoft.AspNetCore.OutputCaching;
using Microsoft.Extensions.Logging;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Cache;
using Umbraco.Cms.Core.Events;
using Umbraco.Cms.Core.Notifications;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Sync;
using Umbraco.Cms.Web.Common.Caching;
namespace Umbraco.Cms.Api.Delivery.Caching;
/// <summary>
/// Handles <see cref="MemberCacheRefresherNotification"/> to evict Delivery API output cache entries
/// for content that references the changed member via picker properties (umbMember relations).
/// </summary>
internal sealed class DeliveryApiMemberOutputCacheEvictionHandler
: RelationOutputCacheEvictionHandlerBase, INotificationAsyncHandler<MemberCacheRefresherNotification>
{
private readonly ILogger<DeliveryApiMemberOutputCacheEvictionHandler> _logger;
/// <summary>
/// Initializes a new instance of the <see cref="DeliveryApiMemberOutputCacheEvictionHandler"/> class.
/// </summary>
/// <param name="outputCacheStore">The output cache store for evicting cached responses.</param>
/// <param name="relationService">The relation service for querying entity references.</param>
/// <param name="idKeyMap">The ID/key mapping service for converting between integer IDs and GUIDs.</param>
/// <param name="logger">The logger.</param>
public DeliveryApiMemberOutputCacheEvictionHandler(
IOutputCacheStore outputCacheStore,
IRelationService relationService,
IIdKeyMap idKeyMap,
ILogger<DeliveryApiMemberOutputCacheEvictionHandler> logger)
: base(outputCacheStore, relationService, idKeyMap)
=> _logger = logger;
/// <inheritdoc />
public async Task HandleAsync(MemberCacheRefresherNotification notification, CancellationToken cancellationToken)
{
if (notification.MessageType != MessageType.RefreshByPayload
|| notification.MessageObject is not MemberCacheRefresher.JsonPayload[] payloads)
{
return;
}
// Evict content that references the changed members via picker properties.
await EvictRelatedContentAsync(
payloads.Select(p => p.Id),
Constants.Conventions.RelationTypes.RelatedMemberAlias,
Constants.DeliveryApi.OutputCache.ContentTagPrefix,
_logger,
cancellationToken);
}
}
@@ -1,47 +0,0 @@
using Microsoft.AspNetCore.OutputCaching;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Primitives;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Models.PublishedContent;
using Umbraco.Cms.Core.Services.Navigation;
namespace Umbraco.Cms.Api.Delivery.Caching;
/// <summary>
/// Output cache policy for Delivery API content endpoints.
/// </summary>
internal sealed class DeliveryApiOutputCacheContentPolicy : DeliveryApiOutputCachePolicyBase
{
/// <summary>
/// Initializes a new instance of the <see cref="DeliveryApiOutputCacheContentPolicy"/> class.
/// </summary>
/// <param name="defaultDuration">The default cache duration from configuration.</param>
/// <param name="defaultVaryByHeaders">The default vary-by headers for content requests.</param>
public DeliveryApiOutputCacheContentPolicy(TimeSpan defaultDuration, StringValues defaultVaryByHeaders)
: base(defaultDuration, defaultVaryByHeaders)
{
}
/// <inheritdoc />
protected override string ResolvedItemsKey => DeliveryApiOutputCacheKeys.ResolvedContentItemsKey;
/// <inheritdoc />
protected override string ItemTagPrefix => Constants.DeliveryApi.OutputCache.ContentTagPrefix;
/// <inheritdoc />
protected override string AllItemsTag => Constants.DeliveryApi.OutputCache.AllContentTag;
/// <inheritdoc />
protected override void AddItemTags(OutputCacheContext context, IPublishedContent item, IServiceProvider services)
{
// Tag with ancestor keys for branch eviction.
IDocumentNavigationQueryService navigationService = services.GetRequiredService<IDocumentNavigationQueryService>();
if (navigationService.TryGetAncestorsKeys(item.Key, out IEnumerable<Guid> ancestorKeys))
{
foreach (Guid ancestorKey in ancestorKeys)
{
context.Tags.Add(Constants.DeliveryApi.OutputCache.AncestorTagPrefix + ancestorKey);
}
}
}
}
@@ -1,18 +0,0 @@
namespace Umbraco.Cms.Api.Delivery.Caching;
/// <summary>
/// Keys used to pass resolved content and media items from controllers to the output cache policy
/// via <see cref="Microsoft.AspNetCore.Http.HttpContext.Items"/>.
/// </summary>
internal static class DeliveryApiOutputCacheKeys
{
/// <summary>
/// Key for storing resolved content items in <see cref="Microsoft.AspNetCore.Http.HttpContext.Items"/>.
/// </summary>
public const string ResolvedContentItemsKey = "Umbraco.DeliveryApi.OutputCache.ResolvedContentItems";
/// <summary>
/// Key for storing resolved media items in <see cref="Microsoft.AspNetCore.Http.HttpContext.Items"/>.
/// </summary>
public const string ResolvedMediaItemsKey = "Umbraco.DeliveryApi.OutputCache.ResolvedMediaItems";
}
@@ -1,45 +0,0 @@
using Microsoft.AspNetCore.OutputCaching;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Cache;
namespace Umbraco.Cms.Api.Delivery.Caching;
/// <summary>
/// Default implementation of <see cref="IDeliveryApiOutputCacheManager"/> that delegates
/// to the ASP.NET Core <see cref="IOutputCacheStore"/>.
/// </summary>
internal sealed class DeliveryApiOutputCacheManager : IDeliveryApiOutputCacheManager
{
private readonly IOutputCacheStore _outputCacheStore;
/// <summary>
/// Initializes a new instance of the <see cref="DeliveryApiOutputCacheManager"/> class.
/// </summary>
/// <param name="outputCacheStore">The ASP.NET Core output cache store.</param>
public DeliveryApiOutputCacheManager(IOutputCacheStore outputCacheStore)
=> _outputCacheStore = outputCacheStore;
/// <inheritdoc />
public async Task EvictContentAsync(Guid contentKey, CancellationToken cancellationToken = default)
=> await _outputCacheStore.EvictByTagAsync(Constants.DeliveryApi.OutputCache.ContentTagPrefix + contentKey, cancellationToken);
/// <inheritdoc />
public async Task EvictMediaAsync(Guid mediaKey, CancellationToken cancellationToken = default)
=> await _outputCacheStore.EvictByTagAsync(Constants.DeliveryApi.OutputCache.MediaTagPrefix + mediaKey, cancellationToken);
/// <inheritdoc />
public async Task EvictByTagAsync(string tag, CancellationToken cancellationToken = default)
=> await _outputCacheStore.EvictByTagAsync(tag, cancellationToken);
/// <inheritdoc />
public async Task EvictAllContentAsync(CancellationToken cancellationToken = default)
=> await _outputCacheStore.EvictByTagAsync(Constants.DeliveryApi.OutputCache.AllContentTag, cancellationToken);
/// <inheritdoc />
public async Task EvictAllMediaAsync(CancellationToken cancellationToken = default)
=> await _outputCacheStore.EvictByTagAsync(Constants.DeliveryApi.OutputCache.AllMediaTag, cancellationToken);
/// <inheritdoc />
public async Task EvictAllAsync(CancellationToken cancellationToken = default)
=> await _outputCacheStore.EvictByTagAsync(Constants.DeliveryApi.OutputCache.AllTag, cancellationToken);
}
@@ -1,29 +0,0 @@
using Microsoft.Extensions.Primitives;
using Umbraco.Cms.Core;
namespace Umbraco.Cms.Api.Delivery.Caching;
/// <summary>
/// Output cache policy for Delivery API media endpoints.
/// </summary>
internal sealed class DeliveryApiOutputCacheMediaPolicy : DeliveryApiOutputCachePolicyBase
{
/// <summary>
/// Initializes a new instance of the <see cref="DeliveryApiOutputCacheMediaPolicy"/> class.
/// </summary>
/// <param name="defaultDuration">The default cache duration from configuration.</param>
/// <param name="defaultVaryByHeaders">The default vary-by headers for media requests.</param>
public DeliveryApiOutputCacheMediaPolicy(TimeSpan defaultDuration, StringValues defaultVaryByHeaders)
: base(defaultDuration, defaultVaryByHeaders)
{
}
/// <inheritdoc />
protected override string ResolvedItemsKey => DeliveryApiOutputCacheKeys.ResolvedMediaItemsKey;
/// <inheritdoc />
protected override string ItemTagPrefix => Constants.DeliveryApi.OutputCache.MediaTagPrefix;
/// <inheritdoc />
protected override string AllItemsTag => Constants.DeliveryApi.OutputCache.AllMediaTag;
}
@@ -0,0 +1,43 @@
using Microsoft.AspNetCore.OutputCaching;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Primitives;
using Umbraco.Cms.Core.DeliveryApi;
namespace Umbraco.Cms.Api.Delivery.Caching;
internal sealed class DeliveryApiOutputCachePolicy : IOutputCachePolicy
{
private readonly TimeSpan _duration;
private readonly StringValues _varyByHeaderNames;
public DeliveryApiOutputCachePolicy(TimeSpan duration, StringValues varyByHeaderNames)
{
_duration = duration;
_varyByHeaderNames = varyByHeaderNames;
}
ValueTask IOutputCachePolicy.CacheRequestAsync(OutputCacheContext context, CancellationToken cancellationToken)
{
IRequestPreviewService requestPreviewService = context
.HttpContext
.RequestServices
.GetRequiredService<IRequestPreviewService>();
IApiAccessService apiAccessService = context
.HttpContext
.RequestServices
.GetRequiredService<IApiAccessService>();
context.EnableOutputCaching = requestPreviewService.IsPreview() is false && apiAccessService.HasPublicAccess();
context.ResponseExpirationTimeSpan = _duration;
context.CacheVaryByRules.HeaderNames = _varyByHeaderNames;
return ValueTask.CompletedTask;
}
ValueTask IOutputCachePolicy.ServeFromCacheAsync(OutputCacheContext context, CancellationToken cancellationToken)
=> ValueTask.CompletedTask;
ValueTask IOutputCachePolicy.ServeResponseAsync(OutputCacheContext context, CancellationToken cancellationToken)
=> ValueTask.CompletedTask;
}
@@ -1,154 +0,0 @@
using Microsoft.AspNetCore.OutputCaching;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Primitives;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Cache;
using Umbraco.Cms.Core.Models.PublishedContent;
namespace Umbraco.Cms.Api.Delivery.Caching;
/// <summary>
/// Base output cache policy for Delivery API endpoints. Handles request filtering, vary-by rules,
/// and tagging. Subclasses specify the resolved-items key, tag prefix, and "all" tag that
/// distinguish content from media.
/// </summary>
internal abstract class DeliveryApiOutputCachePolicyBase : IOutputCachePolicy
{
private readonly TimeSpan _defaultDuration;
private readonly StringValues _defaultVaryByHeaders;
/// <summary>
/// Initializes a new instance of the <see cref="DeliveryApiOutputCachePolicyBase"/> class.
/// </summary>
/// <param name="defaultDuration">The default cache duration from configuration.</param>
/// <param name="defaultVaryByHeaders">The default vary-by headers for this endpoint type.</param>
protected DeliveryApiOutputCachePolicyBase(TimeSpan defaultDuration, StringValues defaultVaryByHeaders)
{
_defaultDuration = defaultDuration;
_defaultVaryByHeaders = defaultVaryByHeaders;
}
/// <summary>
/// Gets the <see cref="Microsoft.AspNetCore.Http.HttpContext.Items"/> key used to retrieve
/// resolved <see cref="IPublishedContent"/> items stashed by the controller.
/// </summary>
protected abstract string ResolvedItemsKey { get; }
/// <summary>
/// Gets the tag prefix for individual item eviction (e.g. <c>umb-dapi-content-</c>).
/// </summary>
protected abstract string ItemTagPrefix { get; }
/// <summary>
/// Gets the "all items" tag for bulk eviction (e.g. <c>umb-dapi-content-all</c>).
/// </summary>
protected abstract string AllItemsTag { get; }
/// <summary>
/// Adds additional per-item tags to the output cache context. Called once per resolved item
/// during <c>ServeResponseAsync</c>. The default implementation does nothing.
/// </summary>
/// <param name="context">The output cache context.</param>
/// <param name="item">The published content or media item.</param>
/// <param name="services">The request service provider.</param>
protected virtual void AddItemTags(OutputCacheContext context, IPublishedContent item, IServiceProvider services)
{
}
/// <inheritdoc />
ValueTask IOutputCachePolicy.CacheRequestAsync(OutputCacheContext context, CancellationToken cancellationToken)
{
IServiceProvider services = context.HttpContext.RequestServices;
ILogger logger = services.GetRequiredService<ILoggerFactory>().CreateLogger(GetType());
IDeliveryApiOutputCacheRequestFilter requestFilter = services.GetRequiredService<IDeliveryApiOutputCacheRequestFilter>();
if (requestFilter.IsCacheable(context.HttpContext) is false)
{
context.EnableOutputCaching = false;
logger.LogDebug("Request filter returned not cacheable — skipping output cache.");
return ValueTask.CompletedTask;
}
context.EnableOutputCaching = true;
context.AllowCacheLookup = true;
context.AllowCacheStorage = true;
context.AllowLocking = true;
context.ResponseExpirationTimeSpan = _defaultDuration;
// Set default vary-by headers.
context.CacheVaryByRules.HeaderNames = _defaultVaryByHeaders;
// Invoke custom vary-by providers (additive, runs after defaults).
IEnumerable<IDeliveryApiOutputCacheVaryByProvider> varyByProviders = services.GetServices<IDeliveryApiOutputCacheVaryByProvider>();
foreach (IDeliveryApiOutputCacheVaryByProvider varyByProvider in varyByProviders)
{
varyByProvider.ConfigureVaryBy(context.HttpContext, context.CacheVaryByRules);
}
// Add base tags for bulk eviction.
context.Tags.Add(AllItemsTag);
context.Tags.Add(Constants.DeliveryApi.OutputCache.AllTag);
return ValueTask.CompletedTask;
}
/// <inheritdoc />
ValueTask IOutputCachePolicy.ServeFromCacheAsync(OutputCacheContext context, CancellationToken cancellationToken)
=> ValueTask.CompletedTask;
/// <inheritdoc />
ValueTask IOutputCachePolicy.ServeResponseAsync(OutputCacheContext context, CancellationToken cancellationToken)
{
if (context.HttpContext.Items[ResolvedItemsKey]
is not IPublishedContent[] items || items.Length == 0)
{
return ValueTask.CompletedTask;
}
IServiceProvider services = context.HttpContext.RequestServices;
ILogger logger = services.GetRequiredService<ILoggerFactory>().CreateLogger(GetType());
IDeliveryApiOutputCacheRequestFilter requestFilter = services.GetRequiredService<IDeliveryApiOutputCacheRequestFilter>();
IEnumerable<IDeliveryApiOutputCacheTagProvider> tagProviders = services.GetServices<IDeliveryApiOutputCacheTagProvider>();
foreach (IPublishedContent item in items)
{
// Check content-aware cacheability.
if (requestFilter.IsCacheable(context.HttpContext, item) is false)
{
context.AllowCacheStorage = false;
if (logger.IsEnabled(LogLevel.Debug))
{
logger.LogDebug("Request filter returned not cacheable for item {ItemKey} — disabling cache storage.", item.Key);
}
return ValueTask.CompletedTask;
}
// Tag with specific item key for targeted eviction.
context.Tags.Add(ItemTagPrefix + item.Key);
// Allow subclasses to add additional per-item tags (e.g. ancestor tags for content).
AddItemTags(context, item, services);
// Invoke custom tag providers.
foreach (IDeliveryApiOutputCacheTagProvider tagProvider in tagProviders)
{
foreach (var tag in tagProvider.GetTags(item))
{
context.Tags.Add(tag);
}
}
}
if (logger.IsEnabled(LogLevel.Debug))
{
logger.LogDebug(
"Caching Delivery API response with {TagCount} tags, duration {Duration}",
context.Tags.Count,
context.ResponseExpirationTimeSpan);
}
return ValueTask.CompletedTask;
}
}
@@ -1,38 +0,0 @@
using Microsoft.AspNetCore.Http;
using Umbraco.Cms.Core.Models.PublishedContent;
namespace Umbraco.Cms.Api.Delivery.Caching;
/// <summary>
/// Determines whether a Delivery API request is eligible for output caching.
/// </summary>
/// <remarks>
/// <para>
/// This interface provides two levels of cacheability checks:
/// </para>
/// <list type="bullet">
/// <item><see cref="IsCacheable(HttpContext)"/> — called before the controller runs, for
/// request-level decisions (e.g. preview mode, access control).</item>
/// <item><see cref="IsCacheable(HttpContext, IPublishedContent)"/> — called after the controller
/// resolves content, for content-aware decisions (e.g. exclude specific content types).</item>
/// </list>
/// </remarks>
public interface IDeliveryApiOutputCacheRequestFilter
{
/// <summary>
/// Gets a value indicating whether the request is eligible for output caching.
/// Called before the controller runs.
/// </summary>
/// <param name="context">The HTTP context for the current request.</param>
/// <returns><c>true</c> if the response may be cached; <c>false</c> to skip caching.</returns>
bool IsCacheable(HttpContext context);
/// <summary>
/// Gets a value indicating whether the response for the given content or media item is eligible
/// for output caching. Called after the controller resolves content.
/// </summary>
/// <param name="context">The HTTP context for the current request.</param>
/// <param name="content">The resolved published content or media item.</param>
/// <returns><c>true</c> if the response may be cached; <c>false</c> to skip caching.</returns>
bool IsCacheable(HttpContext context, IPublishedContent content);
}
@@ -1,28 +0,0 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.OutputCaching;
namespace Umbraco.Cms.Api.Delivery.Caching;
/// <summary>
/// Configures additional vary-by rules for Delivery API output caching.
/// </summary>
/// <remarks>
/// <para>
/// Multiple implementations can be registered; the output cache policy invokes all of them
/// to configure vary-by rules at cache-write time, after the default vary-by headers have been set.
/// </para>
/// <para>
/// Providers have direct access to <see cref="CacheVaryByRules"/> and can configure any aspect
/// including <see cref="CacheVaryByRules.QueryKeys"/>, <see cref="CacheVaryByRules.HeaderNames"/>,
/// and <see cref="CacheVaryByRules.VaryByValues"/>.
/// </para>
/// </remarks>
public interface IDeliveryApiOutputCacheVaryByProvider
{
/// <summary>
/// Configures vary-by rules for the given request.
/// </summary>
/// <param name="context">The HTTP context for the current request.</param>
/// <param name="rules">The vary-by rules to configure.</param>
void ConfigureVaryBy(HttpContext context, CacheVaryByRules rules);
}
@@ -0,0 +1,14 @@
using Microsoft.AspNetCore.Builder;
using Umbraco.Cms.Web.Common.ApplicationBuilder;
namespace Umbraco.Cms.Api.Delivery.Caching;
internal sealed class OutputCachePipelineFilter : UmbracoPipelineFilter
{
public OutputCachePipelineFilter(string name)
: base(name)
=> PostPipeline = PostPipelineAction;
private void PostPipelineAction(IApplicationBuilder applicationBuilder)
=> applicationBuilder.UseOutputCache();
}
@@ -41,20 +41,17 @@ public class ByIdContentApiController : ContentApiItemControllerBase
{
return NotFound();
}
IActionResult? deniedAccessResult = await HandleMemberAccessAsync(contentItem, _requestMemberAccessService).ConfigureAwait(false);
if (deniedAccessResult is not null)
{
return deniedAccessResult;
}
IApiContentResponse? apiContentResponse = ApiContentResponseBuilder.Build(contentItem);
if (apiContentResponse is null)
{
return NotFound();
}
SetOutputCacheContent(contentItem);
return Ok(apiContentResponse);
}
}
@@ -48,7 +48,6 @@ public class ByIdsContentApiController : ContentApiItemControllerBase
.WhereNotNull()
.ToArray();
SetOutputCacheContent(contentItems);
return Ok(apiContentItems);
}
}
@@ -64,7 +64,6 @@ public class ByRouteContentApiController : ContentApiItemControllerBase
return deniedAccessResult;
}
SetOutputCacheContent(contentItem);
return Ok(ApiContentResponseBuilder.Build(contentItem));
}
@@ -2,12 +2,10 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.OutputCaching;
using Umbraco.Cms.Api.Common.Builders;
using Umbraco.Cms.Api.Delivery.Caching;
using Umbraco.Cms.Api.Delivery.Filters;
using Umbraco.Cms.Api.Delivery.Routing;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.DeliveryApi;
using Umbraco.Cms.Core.Models.PublishedContent;
using Umbraco.Cms.Core.Services.OperationStatus;
namespace Umbraco.Cms.Api.Delivery.Controllers.Content;
@@ -52,13 +50,6 @@ public abstract class ContentApiControllerBase : DeliveryApiControllerBase
.Build()),
};
/// <summary>
/// Stores the resolved content items in the HTTP context for use by the output cache policy.
/// </summary>
/// <param name="items">The resolved published content items.</param>
protected void SetOutputCacheContent(params IPublishedContent[] items)
=> HttpContext.Items[DeliveryApiOutputCacheKeys.ResolvedContentItemsKey] = items;
/// <summary>
/// Creates a 403 Forbidden result.
/// </summary>
@@ -62,11 +62,9 @@ public class QueryContentApiController : ContentApiControllerBase
}
PagedModel<Guid> pagedResult = queryAttempt.Result;
IPublishedContent[] contentItems = ApiPublishedContentCache.GetByIds(pagedResult.Items).ToArray();
IEnumerable<IPublishedContent> contentItems = ApiPublishedContentCache.GetByIds(pagedResult.Items);
IApiContentResponse[] apiContentItems = contentItems.Select(ApiContentResponseBuilder.Build).WhereNotNull().ToArray();
SetOutputCacheContent(contentItems);
var model = new PagedViewModel<IApiContentResponse>
{
Total = pagedResult.Total,
@@ -39,7 +39,6 @@ public class ByIdMediaApiController : MediaApiControllerBase
return NotFound();
}
SetOutputCacheMedia(media);
return Ok(BuildApiMediaWithCrops(media));
}
}
@@ -39,7 +39,6 @@ public class ByIdsMediaApiController : MediaApiControllerBase
.Select(BuildApiMediaWithCrops)
.ToArray();
SetOutputCacheMedia(mediaItems);
return Ok(apiMediaItems);
}
}
@@ -43,7 +43,6 @@ public class ByPathMediaApiController : MediaApiControllerBase
return NotFound();
}
SetOutputCacheMedia(media);
return Ok(BuildApiMediaWithCrops(media));
}
}
@@ -1,7 +1,6 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.OutputCaching;
using Umbraco.Cms.Api.Common.Builders;
using Umbraco.Cms.Api.Delivery.Caching;
using Umbraco.Cms.Api.Delivery.Filters;
using Umbraco.Cms.Api.Delivery.Routing;
using Umbraco.Cms.Core;
@@ -34,13 +33,6 @@ public abstract class MediaApiControllerBase : DeliveryApiControllerBase
protected IApiMediaWithCropsResponse BuildApiMediaWithCrops(IPublishedContent media)
=> _apiMediaWithCropsResponseBuilder.Build(media);
/// <summary>
/// Stores the resolved media items in the HTTP context for use by the output cache policy.
/// </summary>
/// <param name="items">The resolved published media items.</param>
protected void SetOutputCacheMedia(params IPublishedContent[] items)
=> HttpContext.Items[DeliveryApiOutputCacheKeys.ResolvedMediaItemsKey] = items;
protected IActionResult ApiMediaQueryOperationStatusResult(ApiMediaQueryOperationStatus status) =>
status switch
{
@@ -59,8 +59,6 @@ public class QueryMediaApiController : MediaApiControllerBase
PagedModel<Guid> pagedResult = queryAttempt.Result;
IPublishedContent[] mediaItems = pagedResult.Items.Select(PublishedMediaCache.GetById).WhereNotNull().ToArray();
SetOutputCacheMedia(mediaItems);
var model = new PagedViewModel<IApiMediaWithCropsResponse>
{
Total = pagedResult.Total,
@@ -5,7 +5,6 @@ using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Primitives;
using Umbraco.Cms.Api.Common.DependencyInjection;
using Umbraco.Cms.Api.Delivery.Accessors;
@@ -19,7 +18,6 @@ using Umbraco.Cms.Api.Delivery.Security;
using Umbraco.Cms.Api.Delivery.Services;
using Umbraco.Cms.Api.Delivery.Services.QueryBuilders;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Cache;
using Umbraco.Cms.Core.Configuration.Models;
using Umbraco.Cms.Core.DeliveryApi;
using Umbraco.Cms.Core.DependencyInjection;
@@ -107,10 +105,6 @@ public static class UmbracoBuilderExtensions
builder.AddNotificationAsyncHandler<MemberDeletedNotification, RevokeMemberAuthenticationTokensNotificationHandler>();
builder.AddNotificationAsyncHandler<AssignedMemberRolesNotification, RevokeMemberAuthenticationTokensNotificationHandler>();
builder.AddNotificationAsyncHandler<RemovedMemberRolesNotification, RevokeMemberAuthenticationTokensNotificationHandler>();
builder.AddNotificationAsyncHandler<ExternalMemberSavedNotification, RevokeMemberAuthenticationTokensNotificationHandler>();
builder.AddNotificationAsyncHandler<ExternalMemberDeletedNotification, RevokeMemberAuthenticationTokensNotificationHandler>();
builder.AddNotificationAsyncHandler<AssignedExternalMemberRolesNotification, RevokeMemberAuthenticationTokensNotificationHandler>();
builder.AddNotificationAsyncHandler<RemovedExternalMemberRolesNotification, RevokeMemberAuthenticationTokensNotificationHandler>();
// FIXME: remove this when Delivery API V1 is removed
builder.Services.AddSingleton<MatcherPolicy, DeliveryApiItemsEndpointsMatcherPolicy>();
@@ -138,7 +132,7 @@ public static class UmbracoBuilderExtensions
{
options.AddPolicy(
Constants.DeliveryApi.OutputCache.ContentCachePolicy,
new DeliveryApiOutputCacheContentPolicy(
new DeliveryApiOutputCachePolicy(
outputCacheSettings.ContentDuration,
new StringValues([Constants.DeliveryApi.HeaderNames.AcceptLanguage, Constants.DeliveryApi.HeaderNames.AcceptSegment, Constants.DeliveryApi.HeaderNames.StartItem])));
}
@@ -147,28 +141,13 @@ public static class UmbracoBuilderExtensions
{
options.AddPolicy(
Constants.DeliveryApi.OutputCache.MediaCachePolicy,
new DeliveryApiOutputCacheMediaPolicy(
new DeliveryApiOutputCachePolicy(
outputCacheSettings.MediaDuration,
Constants.DeliveryApi.HeaderNames.StartItem));
}
});
// Register eviction handlers.
builder.AddNotificationAsyncHandler<ContentCacheRefresherNotification, DeliveryApiDocumentOutputCacheEvictionHandler>();
builder.AddNotificationAsyncHandler<MediaCacheRefresherNotification, DeliveryApiMediaOutputCacheEvictionHandler>();
builder.AddNotificationAsyncHandler<MemberCacheRefresherNotification, DeliveryApiMemberOutputCacheEvictionHandler>();
// Register extension point default implementations.
builder.Services.AddSingleton<IDeliveryApiOutputCacheTagProvider, DeliveryApiContentTypeOutputCacheTagProvider>();
builder.Services.AddUnique<IDeliveryApiOutputCacheRequestFilter, DefaultDeliveryApiOutputCacheRequestFilter>();
builder.Services.AddUnique<IDeliveryApiOutputCacheManager, DeliveryApiOutputCacheManager>();
// Signal that Umbraco has enabled output caching so the application builder registers
// the output cache middleware. Gated via a marker rather than IOutputCacheStore so that
// applications calling services.AddOutputCache(...) for their own purposes are not
// affected by Umbraco's automatic middleware registration.
builder.Services.TryAddSingleton<IUmbracoManagedOutputCacheMarker, UmbracoManagedOutputCacheMarker>();
builder.Services.Configure<UmbracoPipelineOptions>(options => options.AddFilter(new OutputCachePipelineFilter("UmbracoDeliveryApiOutputCache")));
return builder;
}
}
@@ -5,7 +5,6 @@ using Umbraco.Cms.Core.Configuration.Models;
using Umbraco.Cms.Core.Events;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Notifications;
using Umbraco.Cms.Core.Security;
using Umbraco.Cms.Core.Services;
namespace Umbraco.Cms.Api.Delivery.Handlers;
@@ -14,11 +13,7 @@ internal sealed class RevokeMemberAuthenticationTokensNotificationHandler
: INotificationAsyncHandler<MemberSavedNotification>,
INotificationAsyncHandler<MemberDeletedNotification>,
INotificationAsyncHandler<AssignedMemberRolesNotification>,
INotificationAsyncHandler<RemovedMemberRolesNotification>,
INotificationAsyncHandler<ExternalMemberSavedNotification>,
INotificationAsyncHandler<ExternalMemberDeletedNotification>,
INotificationAsyncHandler<AssignedExternalMemberRolesNotification>,
INotificationAsyncHandler<RemovedExternalMemberRolesNotification>
INotificationAsyncHandler<RemovedMemberRolesNotification>
{
private readonly IMemberService _memberService;
private readonly IOpenIddictTokenManager _tokenManager;
@@ -85,38 +80,6 @@ internal sealed class RevokeMemberAuthenticationTokensNotificationHandler
}
}
public async Task HandleAsync(ExternalMemberSavedNotification notification, CancellationToken cancellationToken)
{
if (_enabled is false)
{
return;
}
foreach (ExternalMemberIdentity member in notification.SavedEntities.Where(m => m.IsLockedOut || m.IsApproved is false))
{
await RevokeTokensByKeyAsync(member.Key);
}
}
public async Task HandleAsync(ExternalMemberDeletedNotification notification, CancellationToken cancellationToken)
{
if (_enabled is false)
{
return;
}
foreach (ExternalMemberIdentity member in notification.DeletedEntities)
{
await RevokeTokensByKeyAsync(member.Key);
}
}
public async Task HandleAsync(AssignedExternalMemberRolesNotification notification, CancellationToken cancellationToken)
=> await ExternalMemberRolesChangedAsync(notification);
public async Task HandleAsync(RemovedExternalMemberRolesNotification notification, CancellationToken cancellationToken)
=> await ExternalMemberRolesChangedAsync(notification);
private async Task MemberRolesChangedAsync(MemberRolesNotification notification)
{
if (_enabled is false)
@@ -136,32 +99,4 @@ internal sealed class RevokeMemberAuthenticationTokensNotificationHandler
await RevokeTokensAsync(member);
}
}
private async Task ExternalMemberRolesChangedAsync(ExternalMemberRolesNotification notification)
{
if (_enabled is false)
{
return;
}
foreach (Guid memberKey in notification.MemberKeys)
{
await RevokeTokensByKeyAsync(memberKey);
}
}
private async Task RevokeTokensByKeyAsync(Guid memberKey)
{
var tokens = await _tokenManager.FindBySubjectAsync(memberKey.ToString()).ToArrayAsync();
if (tokens.Any() is false)
{
return;
}
_logger.LogInformation("Revoking {count} active tokens for external member with key {key}", tokens.Length, memberKey);
foreach (var token in tokens)
{
await _tokenManager.DeleteAsync(token);
}
}
}
@@ -40,15 +40,10 @@ public class BackOfficeLoginController : Controller
/// <param name="cancellationToken">A cancellation token to cancel the operation.</param>
/// <param name="model">The model containing login information and the return URL.</param>
/// <returns>
/// An <see cref="IActionResult"/> that renders the login view with the model, or a bad request result if the model state or return URL is invalid.
/// An <see cref="IActionResult"/> that renders the login view with the model, or a bad request result if the return URL is invalid.
/// </returns>
public async Task<IActionResult> Index(CancellationToken cancellationToken, BackOfficeLoginModel model)
{
if (ModelState.IsValid is false)
{
return BadRequest();
}
AuthenticateResult cookieAuthResult = await HttpContext.AuthenticateAsync(Constants.Security.BackOfficeAuthenticationType);
if (cookieAuthResult.Succeeded)
{
@@ -53,14 +53,11 @@ public class SearchDataTypeItemController : DatatypeItemControllerBase
return Ok(new PagedModel<DataTypeItemResponseModel> { Total = searchResult.Total });
}
Guid[] keys = searchResult.Items.Select(x => x.Key).ToArray();
IEnumerable<IDataType> dataTypes = await _dataTypeService.GetAllAsync(keys);
IEnumerable<IDataType> orderedDataTypes = OrderByRequestedIds(dataTypes, keys);
IEnumerable<IDataType> dataTypes = await _dataTypeService.GetAllAsync(searchResult.Items.Select(item => item.Key).ToArray());
var result = new PagedModel<DataTypeItemResponseModel>
{
Items = _mapper.MapEnumerable<IDataType, DataTypeItemResponseModel>(orderedDataTypes),
Total = searchResult.Total,
Items = _mapper.MapEnumerable<IDataType, DataTypeItemResponseModel>(dataTypes),
Total = searchResult.Total
};
return Ok(result);
@@ -1,100 +0,0 @@
using Asp.Versioning;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Api.Management.Security.Authorization;
using Umbraco.Cms.Api.Management.ViewModels.Sorting;
using Umbraco.Cms.Core.Actions;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Security;
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;
/// <summary>
/// Provides an API endpoint for sorting the root-level documents by a system field.
/// </summary>
[ApiVersion("1.0")]
public class SortChildrenAtRootDocumentController : DocumentControllerBase
{
private readonly IAuthorizationService _authorizationService;
private readonly IContentEditingService _contentEditingService;
private readonly IEntityService _entityService;
private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor;
/// <summary>
/// Initializes a new instance of the <see cref="SortChildrenAtRootDocumentController"/> class.
/// </summary>
/// <param name="authorizationService">Service used to authorize user actions.</param>
/// <param name="contentEditingService">Service for editing and managing content.</param>
/// <param name="entityService">Service used to resolve the children to authorize.</param>
/// <param name="backOfficeSecurityAccessor">Accessor for back office security context.</param>
public SortChildrenAtRootDocumentController(
IAuthorizationService authorizationService,
IContentEditingService contentEditingService,
IEntityService entityService,
IBackOfficeSecurityAccessor backOfficeSecurityAccessor)
{
_authorizationService = authorizationService;
_contentEditingService = contentEditingService;
_entityService = entityService;
_backOfficeSecurityAccessor = backOfficeSecurityAccessor;
}
/// <summary>
/// Sorts the root-level documents by a system field.
/// </summary>
/// <param name="cancellationToken">A <see cref="CancellationToken"/> to observe while waiting for the task to complete.</param>
/// <param name="requestModel">The field to sort by and the sort direction.</param>
/// <returns>
/// An <see cref="IActionResult"/> indicating the outcome of the operation:
/// returns <c>200 OK</c> if sorting succeeds or <c>400 Bad Request</c> if the field is not recognised.
/// </returns>
[HttpPut("root/sort-children")]
[MapToApiVersion("1.0")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)]
[EndpointSummary("Sorts the root-level documents by a field.")]
[EndpointDescription("Sorts the root-level documents by a system field in the given direction. When sorting by name, an optional culture selects the variant name; the culture is not validated, so documents that do not vary by it (or an unrecognised culture) fall back to the invariant name.")]
public async Task<IActionResult> SortChildren(CancellationToken cancellationToken, SortDocumentChildrenByFieldRequestModel requestModel)
{
AuthorizationResult authorizationResult = await _authorizationService.AuthorizeResourceAsync(
User,
ContentPermissionResource.WithKeys(ActionSort.ActionLetter, (Guid?)null),
AuthorizationPolicies.ContentPermissionByResource);
if (!authorizationResult.Succeeded)
{
return Forbidden();
}
var childrenAuthorized = await AllChildrenAuthorizer.IsAuthorizedForChildrenAsync(
_authorizationService,
_entityService,
User,
parentKey: null,
UmbracoObjectTypes.Document,
childKeys => ContentPermissionResource.WithKeys(ActionSort.ActionLetter, childKeys),
AuthorizationPolicies.ContentPermissionByResource);
if (!childrenAuthorized)
{
return Forbidden();
}
ContentEditingOperationStatus result = await _contentEditingService.SortByFieldAsync(
null,
requestModel.Field,
requestModel.Direction,
requestModel.Culture,
CurrentUserKey(_backOfficeSecurityAccessor));
return result == ContentEditingOperationStatus.Success
? Ok()
: ContentEditingOperationStatusResult(result);
}
}
@@ -1,102 +0,0 @@
using Asp.Versioning;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Api.Management.Security.Authorization;
using Umbraco.Cms.Api.Management.ViewModels.Sorting;
using Umbraco.Cms.Core.Actions;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Security;
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;
/// <summary>
/// Provides an API endpoint for sorting the children of a document by a system field.
/// </summary>
[ApiVersion("1.0")]
public class SortChildrenDocumentController : DocumentControllerBase
{
private readonly IAuthorizationService _authorizationService;
private readonly IContentEditingService _contentEditingService;
private readonly IEntityService _entityService;
private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor;
/// <summary>
/// Initializes a new instance of the <see cref="SortChildrenDocumentController"/> class.
/// </summary>
/// <param name="authorizationService">Service used to authorize user actions.</param>
/// <param name="contentEditingService">Service for editing and managing content.</param>
/// <param name="entityService">Service used to resolve the children to authorize.</param>
/// <param name="backOfficeSecurityAccessor">Accessor for back office security context.</param>
public SortChildrenDocumentController(
IAuthorizationService authorizationService,
IContentEditingService contentEditingService,
IEntityService entityService,
IBackOfficeSecurityAccessor backOfficeSecurityAccessor)
{
_authorizationService = authorizationService;
_contentEditingService = contentEditingService;
_entityService = entityService;
_backOfficeSecurityAccessor = backOfficeSecurityAccessor;
}
/// <summary>
/// Sorts the child documents of the specified parent document by a system field.
/// </summary>
/// <param name="cancellationToken">A <see cref="CancellationToken"/> to observe while waiting for the task to complete.</param>
/// <param name="id">The unique identifier of the parent document whose children should be sorted.</param>
/// <param name="requestModel">The field to sort by and the sort direction.</param>
/// <returns>
/// An <see cref="IActionResult"/> indicating the outcome of the operation:
/// returns <c>200 OK</c> if sorting succeeds, <c>400 Bad Request</c> if the field is not recognised, or <c>404 Not Found</c> if the parent document does not exist.
/// </returns>
[HttpPut("{id:guid}/sort-children")]
[MapToApiVersion("1.0")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
[EndpointSummary("Sorts the children of a document by a field.")]
[EndpointDescription("Sorts the children of the specified parent document by a system field in the given direction. When sorting by name, an optional culture selects the variant name; the culture is not validated, so children that do not vary by it (or an unrecognised culture) fall back to the invariant name.")]
public async Task<IActionResult> SortChildren(CancellationToken cancellationToken, Guid id, SortDocumentChildrenByFieldRequestModel requestModel)
{
AuthorizationResult authorizationResult = await _authorizationService.AuthorizeResourceAsync(
User,
ContentPermissionResource.WithKeys(ActionSort.ActionLetter, id),
AuthorizationPolicies.ContentPermissionByResource);
if (!authorizationResult.Succeeded)
{
return Forbidden();
}
var childrenAuthorized = await AllChildrenAuthorizer.IsAuthorizedForChildrenAsync(
_authorizationService,
_entityService,
User,
id,
UmbracoObjectTypes.Document,
childKeys => ContentPermissionResource.WithKeys(ActionSort.ActionLetter, childKeys),
AuthorizationPolicies.ContentPermissionByResource);
if (!childrenAuthorized)
{
return Forbidden();
}
ContentEditingOperationStatus result = await _contentEditingService.SortByFieldAsync(
id,
requestModel.Field,
requestModel.Direction,
requestModel.Culture,
CurrentUserKey(_backOfficeSecurityAccessor));
return result == ContentEditingOperationStatus.Success
? Ok()
: ContentEditingOperationStatusResult(result);
}
}
@@ -1,4 +1,3 @@
using System.ComponentModel;
using Asp.Versioning;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
@@ -20,68 +19,6 @@ namespace Umbraco.Cms.Api.Management.Controllers.Document.Tree;
[ApiVersion("1.0")]
public class AncestorsDocumentTreeController : DocumentTreeControllerBase
{
/// <summary>
/// Initializes a new instance of the <see cref="AncestorsDocumentTreeController"/> class.
/// </summary>
/// <param name="entityService">Service for managing and retrieving entities in the system.</param>
/// <param name="flagProviders">A collection of providers that supply flags for document tree nodes.</param>
/// <param name="treeFilterService">Service for filtering document tree entities based on user start nodes.</param>
/// <param name="publicAccessService">Service for handling public access permissions on documents.</param>
/// <param name="documentPresentationFactory">Factory for creating document presentation models.</param>
/// <param name="documentPermissionFilterService">Service for filtering documents based on user permissions.</param>
[ActivatorUtilitiesConstructor]
public AncestorsDocumentTreeController(
IEntityService entityService,
FlagProviderCollection flagProviders,
IDocumentStartNodeTreeFilterService treeFilterService,
IPublicAccessService publicAccessService,
IDocumentPresentationFactory documentPresentationFactory,
IDocumentPermissionFilterService documentPermissionFilterService)
: base(
entityService,
flagProviders,
treeFilterService,
publicAccessService,
documentPresentationFactory,
documentPermissionFilterService)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="AncestorsDocumentTreeController"/> class.
/// </summary>
/// <remarks>
/// This constructor exists solely to disambiguate DI container constructor resolution between the new
/// and the existing obsolete constructors; all parameters except those forwarded to the non-obsolete
/// constructor are ignored.
/// </remarks>
/// <param name="entityService">Service for managing and retrieving entities within Umbraco.</param>
/// <param name="flagProviders">A collection of providers that supply flags for document tree nodes.</param>
/// <param name="userStartNodeEntitiesService">Service for resolving user start nodes for entities.</param>
/// <param name="dataTypeService">Service for accessing and managing data types.</param>
/// <param name="treeFilterService">Service for filtering document tree entities based on user start nodes.</param>
/// <param name="publicAccessService">Service for handling public access permissions on documents.</param>
/// <param name="appCaches">Provides access to application-level caches.</param>
/// <param name="backofficeSecurityAccessor">Accessor for backoffice security context and operations.</param>
/// <param name="documentPresentationFactory">Factory for creating document presentation models.</param>
/// <param name="documentPermissionFilterService">Service for filtering documents based on user permissions.</param>
[Obsolete("Please use the non-obsolete constructor. Scheduled for removal in Umbraco 19.")]
[EditorBrowsable(EditorBrowsableState.Never)]
public AncestorsDocumentTreeController(
IEntityService entityService,
FlagProviderCollection flagProviders,
IUserStartNodeEntitiesService userStartNodeEntitiesService,
IDataTypeService dataTypeService,
IDocumentStartNodeTreeFilterService treeFilterService,
IPublicAccessService publicAccessService,
AppCaches appCaches,
IBackOfficeSecurityAccessor backofficeSecurityAccessor,
IDocumentPresentationFactory documentPresentationFactory,
IDocumentPermissionFilterService documentPermissionFilterService)
: this(entityService, flagProviders, treeFilterService, publicAccessService, documentPresentationFactory, documentPermissionFilterService)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="Umbraco.Cms.Api.Management.Controllers.Document.Tree.AncestorsDocumentTreeController"/> class.
/// </summary>
@@ -123,7 +60,7 @@ public class AncestorsDocumentTreeController : DocumentTreeControllerBase
/// <param name="appCaches">Provides access to application-level caches.</param>
/// <param name="backofficeSecurityAccessor">Accessor for backoffice security context and operations.</param>
/// <param name="documentPresentationFactory">Factory for creating document presentation models.</param>
[Obsolete("Please use the constructor accepting IDocumentStartNodeTreeFilterService. Scheduled for removal in Umbraco 19.")]
[Obsolete("Please use the constructor taking all parameters. Scheduled for removal in Umbraco 19.")]
public AncestorsDocumentTreeController(
IEntityService entityService,
FlagProviderCollection flagProviders,
@@ -157,7 +94,7 @@ public class AncestorsDocumentTreeController : DocumentTreeControllerBase
/// <param name="backofficeSecurityAccessor">Accessor for backoffice security context and authentication.</param>
/// <param name="documentPresentationFactory">Factory for creating document presentation models.</param>
/// <param name="documentPermissionFilterService">Service for filtering documents based on user permissions.</param>
[Obsolete("Please use the constructor accepting IDocumentStartNodeTreeFilterService. Scheduled for removal in Umbraco 19.")]
[ActivatorUtilitiesConstructor]
public AncestorsDocumentTreeController(
IEntityService entityService,
FlagProviderCollection flagProviders,
@@ -1,4 +1,3 @@
using System.ComponentModel;
using Asp.Versioning;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
@@ -21,68 +20,6 @@ namespace Umbraco.Cms.Api.Management.Controllers.Document.Tree;
[ApiVersion("1.0")]
public class ChildrenDocumentTreeController : DocumentTreeControllerBase
{
/// <summary>
/// Initializes a new instance of the <see cref="ChildrenDocumentTreeController"/> class.
/// </summary>
/// <param name="entityService">Service for managing and retrieving entities in the system.</param>
/// <param name="flagProviders">A collection of providers that supply flags for document tree nodes.</param>
/// <param name="treeFilterService">Service for filtering document tree entities based on user start nodes.</param>
/// <param name="publicAccessService">Service for handling public access permissions on documents.</param>
/// <param name="documentPresentationFactory">Factory for creating document presentation models.</param>
/// <param name="documentPermissionFilterService">Service for filtering documents based on user permissions.</param>
[ActivatorUtilitiesConstructor]
public ChildrenDocumentTreeController(
IEntityService entityService,
FlagProviderCollection flagProviders,
IDocumentStartNodeTreeFilterService treeFilterService,
IPublicAccessService publicAccessService,
IDocumentPresentationFactory documentPresentationFactory,
IDocumentPermissionFilterService documentPermissionFilterService)
: base(
entityService,
flagProviders,
treeFilterService,
publicAccessService,
documentPresentationFactory,
documentPermissionFilterService)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="ChildrenDocumentTreeController"/> class.
/// </summary>
/// <remarks>
/// This constructor exists solely to disambiguate DI container constructor resolution between the new
/// and the existing obsolete constructors; all parameters except those forwarded to the non-obsolete
/// constructor are ignored.
/// </remarks>
/// <param name="entityService">Service for managing and retrieving entities within Umbraco.</param>
/// <param name="flagProviders">A collection of providers that supply flags for document tree nodes.</param>
/// <param name="userStartNodeEntitiesService">Service for resolving user start nodes for entities.</param>
/// <param name="dataTypeService">Service for accessing and managing data types.</param>
/// <param name="treeFilterService">Service for filtering document tree entities based on user start nodes.</param>
/// <param name="publicAccessService">Service for handling public access permissions on documents.</param>
/// <param name="appCaches">Provides access to application-level caches.</param>
/// <param name="backofficeSecurityAccessor">Accessor for backoffice security context and operations.</param>
/// <param name="documentPresentationFactory">Factory for creating document presentation models.</param>
/// <param name="documentPermissionFilterService">Service for filtering documents based on user permissions.</param>
[Obsolete("Please use the non-obsolete constructor. Scheduled for removal in Umbraco 19.")]
[EditorBrowsable(EditorBrowsableState.Never)]
public ChildrenDocumentTreeController(
IEntityService entityService,
FlagProviderCollection flagProviders,
IUserStartNodeEntitiesService userStartNodeEntitiesService,
IDataTypeService dataTypeService,
IDocumentStartNodeTreeFilterService treeFilterService,
IPublicAccessService publicAccessService,
AppCaches appCaches,
IBackOfficeSecurityAccessor backofficeSecurityAccessor,
IDocumentPresentationFactory documentPresentationFactory,
IDocumentPermissionFilterService documentPermissionFilterService)
: this(entityService, flagProviders, treeFilterService, publicAccessService, documentPresentationFactory, documentPermissionFilterService)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="ChildrenDocumentTreeController"/> class.
/// </summary>
@@ -124,7 +61,7 @@ public class ChildrenDocumentTreeController : DocumentTreeControllerBase
/// <param name="appCaches">Provides application-level caching functionality.</param>
/// <param name="backofficeSecurityAccessor">Accessor for backoffice security context and authentication.</param>
/// <param name="documentPresentationFactory">Factory for creating document presentation models for the API.</param>
[Obsolete("Please use the constructor accepting IDocumentStartNodeTreeFilterService. Scheduled for removal in Umbraco 19.")]
[Obsolete("Please use the constructor taking all parameters. Scheduled for removal in Umbraco 19.")]
public ChildrenDocumentTreeController(
IEntityService entityService,
FlagProviderCollection flagProviders,
@@ -158,7 +95,7 @@ public class ChildrenDocumentTreeController : DocumentTreeControllerBase
/// <param name="backofficeSecurityAccessor">Accessor for backoffice security context.</param>
/// <param name="documentPresentationFactory">Factory for creating document presentation models.</param>
/// <param name="documentPermissionFilterService">Service for filtering documents based on permissions.</param>
[Obsolete("Please use the constructor accepting IDocumentStartNodeTreeFilterService. Scheduled for removal in Umbraco 19.")]
[ActivatorUtilitiesConstructor]
public ChildrenDocumentTreeController(
IEntityService entityService,
FlagProviderCollection flagProviders,
@@ -29,14 +29,11 @@ namespace Umbraco.Cms.Api.Management.Controllers.Document.Tree;
public abstract class DocumentTreeControllerBase : UserStartNodeTreeControllerBase<DocumentTreeItemResponseModel>
{
private readonly IPublicAccessService _publicAccessService;
private readonly AppCaches _appCaches;
private readonly IBackOfficeSecurityAccessor _backofficeSecurityAccessor;
private readonly IDocumentPresentationFactory _documentPresentationFactory;
private readonly IDocumentPermissionFilterService _documentPermissionFilterService;
// Only populated by the obsolete constructor path; used solely by the obsolete
// GetUserStartNodeIds / GetUserStartNodePaths overrides below.
private readonly AppCaches? _appCaches;
private readonly IBackOfficeSecurityAccessor? _backofficeSecurityAccessor;
[Obsolete("Please use the constructor taking all parameters. Scheduled for removal in Umbraco 18.")]
protected DocumentTreeControllerBase(
IEntityService entityService,
@@ -58,7 +55,7 @@ public abstract class DocumentTreeControllerBase : UserStartNodeTreeControllerBa
{
}
[Obsolete("Please use the constructor accepting IDocumentStartNodeTreeFilterService. Scheduled for removal in Umbraco 19.")]
[Obsolete("Please use the constructor taking all parameters. Scheduled for removal in Umbraco 19.")]
protected DocumentTreeControllerBase(
IEntityService entityService,
FlagProviderCollection flagProviders,
@@ -81,7 +78,7 @@ public abstract class DocumentTreeControllerBase : UserStartNodeTreeControllerBa
{
}
[Obsolete("Please use the constructor accepting IDocumentStartNodeTreeFilterService. Scheduled for removal in Umbraco 19.")]
[ActivatorUtilitiesConstructor]
protected DocumentTreeControllerBase(
IEntityService entityService,
FlagProviderCollection flagProviders,
@@ -101,30 +98,6 @@ public abstract class DocumentTreeControllerBase : UserStartNodeTreeControllerBa
_documentPermissionFilterService = documentPermissionFilterService;
}
/// <summary>
/// Initializes a new instance of the <see cref="DocumentTreeControllerBase"/> class.
/// </summary>
/// <param name="entityService">Service for managing and retrieving entities in the system.</param>
/// <param name="flagProviders">A collection of providers that supply flags for document tree nodes.</param>
/// <param name="treeFilterService">Service for filtering document tree entities based on user start nodes.</param>
/// <param name="publicAccessService">Service for handling public access permissions on documents.</param>
/// <param name="documentPresentationFactory">Factory for creating document presentation models.</param>
/// <param name="documentPermissionFilterService">Service for filtering documents based on user permissions.</param>
[ActivatorUtilitiesConstructor]
protected DocumentTreeControllerBase(
IEntityService entityService,
FlagProviderCollection flagProviders,
IDocumentStartNodeTreeFilterService treeFilterService,
IPublicAccessService publicAccessService,
IDocumentPresentationFactory documentPresentationFactory,
IDocumentPermissionFilterService documentPermissionFilterService)
: base(entityService, flagProviders, treeFilterService)
{
_publicAccessService = publicAccessService;
_documentPresentationFactory = documentPresentationFactory;
_documentPermissionFilterService = documentPermissionFilterService;
}
protected override UmbracoObjectTypes ItemObjectType => UmbracoObjectTypes.Document;
protected override Ordering ItemOrdering => Ordering.By(Infrastructure.Persistence.Dtos.NodeDto.SortOrderColumnName);
@@ -149,27 +122,21 @@ public abstract class DocumentTreeControllerBase : UserStartNodeTreeControllerBa
return responseModel;
}
// Only invoked via the CallbackStartNodeTreeFilterService wired up by the obsolete
// UserStartNodeTreeControllerBase constructor. The non-obsolete constructor path
// routes start node resolution through IDocumentStartNodeTreeFilterService and
// never calls these overrides; hence the null-forgiving operator on _appCaches.
/// <inheritdoc/>
[Obsolete("No longer used. Register a custom IDocumentStartNodeTreeFilterService instead. Scheduled for removal in Umbraco 19.")]
protected override int[] GetUserStartNodeIds()
=> _backofficeSecurityAccessor?
=> _backofficeSecurityAccessor
.BackOfficeSecurity?
.CurrentUser?
.CalculateContentStartNodeIds(EntityService, _appCaches!)
?? [];
.CalculateContentStartNodeIds(EntityService, _appCaches)
?? Array.Empty<int>();
/// <inheritdoc/>
[Obsolete("No longer used. Register a custom IDocumentStartNodeTreeFilterService instead. Scheduled for removal in Umbraco 19.")]
protected override string[] GetUserStartNodePaths()
=> _backofficeSecurityAccessor?
=> _backofficeSecurityAccessor
.BackOfficeSecurity?
.CurrentUser?
.GetContentStartNodePaths(EntityService, _appCaches!)
?? [];
.GetContentStartNodePaths(EntityService, _appCaches)
?? Array.Empty<string>();
/// <inheritdoc/>
protected override Task<(IEntitySlim[] Entities, long TotalItems)> FilterTreeEntities(IEntitySlim[] entities, long totalItems)
@@ -1,4 +1,3 @@
using System.ComponentModel;
using Asp.Versioning;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
@@ -21,68 +20,6 @@ namespace Umbraco.Cms.Api.Management.Controllers.Document.Tree;
[ApiVersion("1.0")]
public class RootDocumentTreeController : DocumentTreeControllerBase
{
/// <summary>
/// Initializes a new instance of the <see cref="RootDocumentTreeController"/> class.
/// </summary>
/// <param name="entityService">Service for managing and retrieving entities in the system.</param>
/// <param name="flagProviders">A collection of providers that supply flags for document tree nodes.</param>
/// <param name="treeFilterService">Service for filtering document tree entities based on user start nodes.</param>
/// <param name="publicAccessService">Service for handling public access permissions on documents.</param>
/// <param name="documentPresentationFactory">Factory for creating document presentation models.</param>
/// <param name="documentPermissionFilterService">Service for filtering documents based on user permissions.</param>
[ActivatorUtilitiesConstructor]
public RootDocumentTreeController(
IEntityService entityService,
FlagProviderCollection flagProviders,
IDocumentStartNodeTreeFilterService treeFilterService,
IPublicAccessService publicAccessService,
IDocumentPresentationFactory documentPresentationFactory,
IDocumentPermissionFilterService documentPermissionFilterService)
: base(
entityService,
flagProviders,
treeFilterService,
publicAccessService,
documentPresentationFactory,
documentPermissionFilterService)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="RootDocumentTreeController"/> class.
/// </summary>
/// <remarks>
/// This constructor exists solely to disambiguate DI container constructor resolution between the new
/// and the existing obsolete constructors; all parameters except those forwarded to the non-obsolete
/// constructor are ignored.
/// </remarks>
/// <param name="entityService">Service for managing and retrieving entities within Umbraco.</param>
/// <param name="flagProviders">A collection of providers that supply flags for document tree nodes.</param>
/// <param name="userStartNodeEntitiesService">Service for resolving user start nodes for entities.</param>
/// <param name="dataTypeService">Service for accessing and managing data types.</param>
/// <param name="treeFilterService">Service for filtering document tree entities based on user start nodes.</param>
/// <param name="publicAccessService">Service for handling public access permissions on documents.</param>
/// <param name="appCaches">Provides access to application-level caches.</param>
/// <param name="backofficeSecurityAccessor">Accessor for backoffice security context and operations.</param>
/// <param name="documentPresentationFactory">Factory for creating document presentation models.</param>
/// <param name="documentPermissionFilterService">Service for filtering documents based on user permissions.</param>
[Obsolete("Please use the non-obsolete constructor. Scheduled for removal in Umbraco 19.")]
[EditorBrowsable(EditorBrowsableState.Never)]
public RootDocumentTreeController(
IEntityService entityService,
FlagProviderCollection flagProviders,
IUserStartNodeEntitiesService userStartNodeEntitiesService,
IDataTypeService dataTypeService,
IDocumentStartNodeTreeFilterService treeFilterService,
IPublicAccessService publicAccessService,
AppCaches appCaches,
IBackOfficeSecurityAccessor backofficeSecurityAccessor,
IDocumentPresentationFactory documentPresentationFactory,
IDocumentPermissionFilterService documentPermissionFilterService)
: this(entityService, flagProviders, treeFilterService, publicAccessService, documentPresentationFactory, documentPermissionFilterService)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="RootDocumentTreeController"/> class, which manages the root nodes of the document tree in the Umbraco backoffice.
/// </summary>
@@ -124,7 +61,7 @@ public class RootDocumentTreeController : DocumentTreeControllerBase
/// <param name="appCaches">Provides application-level caching mechanisms.</param>
/// <param name="backofficeSecurityAccessor">Accessor for backoffice security context and operations.</param>
/// <param name="documentPresentationFactory">Factory for creating document presentation models.</param>
[Obsolete("Please use the constructor accepting IDocumentStartNodeTreeFilterService. Scheduled for removal in Umbraco 19.")]
[Obsolete("Please use the constructor taking all parameters. Scheduled for removal in Umbraco 19.")]
public RootDocumentTreeController(
IEntityService entityService,
FlagProviderCollection flagProviders,
@@ -158,7 +95,7 @@ public class RootDocumentTreeController : DocumentTreeControllerBase
/// <param name="backofficeSecurityAccessor">Accessor for backoffice security context and operations.</param>
/// <param name="documentPresentationFactory">Factory for creating document presentation models.</param>
/// <param name="documentPermissionFilterService">Service for filtering documents based on user permissions.</param>
[Obsolete("Please use the constructor accepting IDocumentStartNodeTreeFilterService. Scheduled for removal in Umbraco 19.")]
[ActivatorUtilitiesConstructor]
public RootDocumentTreeController(
IEntityService entityService,
FlagProviderCollection flagProviders,
@@ -1,4 +1,3 @@
using System.ComponentModel;
using Asp.Versioning;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
@@ -21,68 +20,6 @@ namespace Umbraco.Cms.Api.Management.Controllers.Document.Tree;
[ApiVersion("1.0")]
public class SiblingsDocumentTreeController : DocumentTreeControllerBase
{
/// <summary>
/// Initializes a new instance of the <see cref="SiblingsDocumentTreeController"/> class.
/// </summary>
/// <param name="entityService">Service for managing and retrieving entities in the system.</param>
/// <param name="flagProviders">A collection of providers that supply flags for document tree nodes.</param>
/// <param name="treeFilterService">Service for filtering document tree entities based on user start nodes.</param>
/// <param name="publicAccessService">Service for handling public access permissions on documents.</param>
/// <param name="documentPresentationFactory">Factory for creating document presentation models.</param>
/// <param name="documentPermissionFilterService">Service for filtering documents based on user permissions.</param>
[ActivatorUtilitiesConstructor]
public SiblingsDocumentTreeController(
IEntityService entityService,
FlagProviderCollection flagProviders,
IDocumentStartNodeTreeFilterService treeFilterService,
IPublicAccessService publicAccessService,
IDocumentPresentationFactory documentPresentationFactory,
IDocumentPermissionFilterService documentPermissionFilterService)
: base(
entityService,
flagProviders,
treeFilterService,
publicAccessService,
documentPresentationFactory,
documentPermissionFilterService)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="SiblingsDocumentTreeController"/> class.
/// </summary>
/// <remarks>
/// This constructor exists solely to disambiguate DI container constructor resolution between the new
/// and the existing obsolete constructors; all parameters except those forwarded to the non-obsolete
/// constructor are ignored.
/// </remarks>
/// <param name="entityService">Service for managing and retrieving entities within Umbraco.</param>
/// <param name="flagProviders">A collection of providers that supply flags for document tree nodes.</param>
/// <param name="userStartNodeEntitiesService">Service for resolving user start nodes for entities.</param>
/// <param name="dataTypeService">Service for accessing and managing data types.</param>
/// <param name="treeFilterService">Service for filtering document tree entities based on user start nodes.</param>
/// <param name="publicAccessService">Service for handling public access permissions on documents.</param>
/// <param name="appCaches">Provides access to application-level caches.</param>
/// <param name="backofficeSecurityAccessor">Accessor for backoffice security context and operations.</param>
/// <param name="documentPresentationFactory">Factory for creating document presentation models.</param>
/// <param name="documentPermissionFilterService">Service for filtering documents based on user permissions.</param>
[Obsolete("Please use the non-obsolete constructor. Scheduled for removal in Umbraco 19.")]
[EditorBrowsable(EditorBrowsableState.Never)]
public SiblingsDocumentTreeController(
IEntityService entityService,
FlagProviderCollection flagProviders,
IUserStartNodeEntitiesService userStartNodeEntitiesService,
IDataTypeService dataTypeService,
IDocumentStartNodeTreeFilterService treeFilterService,
IPublicAccessService publicAccessService,
AppCaches appCaches,
IBackOfficeSecurityAccessor backofficeSecurityAccessor,
IDocumentPresentationFactory documentPresentationFactory,
IDocumentPermissionFilterService documentPermissionFilterService)
: this(entityService, flagProviders, treeFilterService, publicAccessService, documentPresentationFactory, documentPermissionFilterService)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="SiblingsDocumentTreeController"/> class.
/// </summary>
@@ -124,7 +61,7 @@ public class SiblingsDocumentTreeController : DocumentTreeControllerBase
/// <param name="appCaches">Provides access to application-level caches.</param>
/// <param name="backofficeSecurityAccessor">Accessor for backoffice security context and operations.</param>
/// <param name="documentPresentationFactory">Factory for creating document presentation models.</param>
[Obsolete("Please use the constructor accepting IDocumentStartNodeTreeFilterService. Scheduled for removal in Umbraco 19.")]
[Obsolete("Please use the constructor taking all parameters. Scheduled for removal in Umbraco 19.")]
public SiblingsDocumentTreeController(
IEntityService entityService,
FlagProviderCollection flagProviders,
@@ -158,7 +95,7 @@ public class SiblingsDocumentTreeController : DocumentTreeControllerBase
/// <param name="backofficeSecurityAccessor">Accessor for backoffice security context and user information.</param>
/// <param name="documentPresentationFactory">Factory for creating document presentation models.</param>
/// <param name="documentPermissionFilterService">Service for filtering documents based on user permissions.</param>
[Obsolete("Please use the constructor accepting IDocumentStartNodeTreeFilterService. Scheduled for removal in Umbraco 19.")]
[ActivatorUtilitiesConstructor]
public SiblingsDocumentTreeController(
IEntityService entityService,
FlagProviderCollection flagProviders,
@@ -1,98 +0,0 @@
using Asp.Versioning;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Api.Management.Security.Authorization;
using Umbraco.Cms.Api.Management.ViewModels.Sorting;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Security;
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.Media;
/// <summary>
/// Provides an API endpoint for sorting the root-level media items by a system field.
/// </summary>
[ApiVersion("1.0")]
public class SortChildrenAtRootMediaController : MediaControllerBase
{
private readonly IAuthorizationService _authorizationService;
private readonly IMediaEditingService _mediaEditingService;
private readonly IEntityService _entityService;
private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor;
/// <summary>
/// Initializes a new instance of the <see cref="SortChildrenAtRootMediaController"/> class.
/// </summary>
/// <param name="authorizationService">Service used to authorize user actions.</param>
/// <param name="mediaEditingService">Service responsible for editing and sorting media items.</param>
/// <param name="entityService">Service used to resolve the children to authorize.</param>
/// <param name="backOfficeSecurityAccessor">Accessor for backoffice security context.</param>
public SortChildrenAtRootMediaController(
IAuthorizationService authorizationService,
IMediaEditingService mediaEditingService,
IEntityService entityService,
IBackOfficeSecurityAccessor backOfficeSecurityAccessor)
{
_authorizationService = authorizationService;
_mediaEditingService = mediaEditingService;
_entityService = entityService;
_backOfficeSecurityAccessor = backOfficeSecurityAccessor;
}
/// <summary>
/// Sorts the root-level media items by a system field.
/// </summary>
/// <param name="cancellationToken">A <see cref="CancellationToken"/> to observe while waiting for the task to complete.</param>
/// <param name="requestModel">The field to sort by and the sort direction.</param>
/// <returns>
/// An <see cref="IActionResult"/> indicating the outcome of the operation:
/// returns <c>200 OK</c> if sorting succeeds or <c>400 Bad Request</c> if the field is not recognised.
/// </returns>
[HttpPut("root/sort-children")]
[MapToApiVersion("1.0")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)]
[EndpointSummary("Sorts the root-level media items by a field.")]
[EndpointDescription("Sorts the root-level media items by a system field in the given direction.")]
public async Task<IActionResult> SortChildren(CancellationToken cancellationToken, SortMediaChildrenByFieldRequestModel requestModel)
{
AuthorizationResult authorizationResult = await _authorizationService.AuthorizeResourceAsync(
User,
MediaPermissionResource.Root(),
AuthorizationPolicies.MediaPermissionByResource);
if (!authorizationResult.Succeeded)
{
return Forbidden();
}
var childrenAuthorized = await AllChildrenAuthorizer.IsAuthorizedForChildrenAsync(
_authorizationService,
_entityService,
User,
parentKey: null,
UmbracoObjectTypes.Media,
childKeys => MediaPermissionResource.WithKeys(childKeys),
AuthorizationPolicies.MediaPermissionByResource);
if (!childrenAuthorized)
{
return Forbidden();
}
ContentEditingOperationStatus result = await _mediaEditingService.SortByFieldAsync(
null,
requestModel.Field,
requestModel.Direction,
CurrentUserKey(_backOfficeSecurityAccessor));
return result == ContentEditingOperationStatus.Success
? Ok()
: ContentEditingOperationStatusResult(result);
}
}
@@ -1,100 +0,0 @@
using Asp.Versioning;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Api.Management.Security.Authorization;
using Umbraco.Cms.Api.Management.ViewModels.Sorting;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Security;
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.Media;
/// <summary>
/// Provides an API endpoint for sorting the children of a media item by a system field.
/// </summary>
[ApiVersion("1.0")]
public class SortChildrenMediaController : MediaControllerBase
{
private readonly IAuthorizationService _authorizationService;
private readonly IMediaEditingService _mediaEditingService;
private readonly IEntityService _entityService;
private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor;
/// <summary>
/// Initializes a new instance of the <see cref="SortChildrenMediaController"/> class.
/// </summary>
/// <param name="authorizationService">Service used to authorize user actions.</param>
/// <param name="mediaEditingService">Service responsible for editing and sorting media items.</param>
/// <param name="entityService">Service used to resolve the children to authorize.</param>
/// <param name="backOfficeSecurityAccessor">Accessor for backoffice security context.</param>
public SortChildrenMediaController(
IAuthorizationService authorizationService,
IMediaEditingService mediaEditingService,
IEntityService entityService,
IBackOfficeSecurityAccessor backOfficeSecurityAccessor)
{
_authorizationService = authorizationService;
_mediaEditingService = mediaEditingService;
_entityService = entityService;
_backOfficeSecurityAccessor = backOfficeSecurityAccessor;
}
/// <summary>
/// Sorts the child media items of the specified parent media item by a system field.
/// </summary>
/// <param name="cancellationToken">A <see cref="CancellationToken"/> to observe while waiting for the task to complete.</param>
/// <param name="id">The unique identifier of the parent media item whose children should be sorted.</param>
/// <param name="requestModel">The field to sort by and the sort direction.</param>
/// <returns>
/// An <see cref="IActionResult"/> indicating the outcome of the operation:
/// returns <c>200 OK</c> if sorting succeeds, <c>400 Bad Request</c> if the field is not recognised, or <c>404 Not Found</c> if the parent media item does not exist.
/// </returns>
[HttpPut("{id:guid}/sort-children")]
[MapToApiVersion("1.0")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
[EndpointSummary("Sorts the children of a media item by a field.")]
[EndpointDescription("Sorts the children of the specified parent media item by a system field in the given direction.")]
public async Task<IActionResult> SortChildren(CancellationToken cancellationToken, Guid id, SortMediaChildrenByFieldRequestModel requestModel)
{
AuthorizationResult authorizationResult = await _authorizationService.AuthorizeResourceAsync(
User,
MediaPermissionResource.WithKeys(id),
AuthorizationPolicies.MediaPermissionByResource);
if (!authorizationResult.Succeeded)
{
return Forbidden();
}
var childrenAuthorized = await AllChildrenAuthorizer.IsAuthorizedForChildrenAsync(
_authorizationService,
_entityService,
User,
id,
UmbracoObjectTypes.Media,
childKeys => MediaPermissionResource.WithKeys(childKeys),
AuthorizationPolicies.MediaPermissionByResource);
if (!childrenAuthorized)
{
return Forbidden();
}
ContentEditingOperationStatus result = await _mediaEditingService.SortByFieldAsync(
id,
requestModel.Field,
requestModel.Direction,
CurrentUserKey(_backOfficeSecurityAccessor));
return result == ContentEditingOperationStatus.Success
? Ok()
: ContentEditingOperationStatusResult(result);
}
}
@@ -1,4 +1,3 @@
using System.ComponentModel;
using Asp.Versioning;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
@@ -19,54 +18,6 @@ namespace Umbraco.Cms.Api.Management.Controllers.Media.Tree;
[ApiVersion("1.0")]
public class AncestorsMediaTreeController : MediaTreeControllerBase
{
/// <summary>
/// Initializes a new instance of the <see cref="AncestorsMediaTreeController"/> class.
/// </summary>
/// <param name="entityService">Service for managing and retrieving entities within the system.</param>
/// <param name="flagProviders">A collection of providers that supply flags for entities.</param>
/// <param name="treeFilterService">Service for filtering media tree entities based on user start nodes.</param>
/// <param name="mediaPresentationFactory">Factory for creating media presentation models.</param>
[ActivatorUtilitiesConstructor]
public AncestorsMediaTreeController(
IEntityService entityService,
FlagProviderCollection flagProviders,
IMediaStartNodeTreeFilterService treeFilterService,
IMediaPresentationFactory mediaPresentationFactory)
: base(entityService, flagProviders, treeFilterService, mediaPresentationFactory)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="AncestorsMediaTreeController"/> class.
/// </summary>
/// <remarks>
/// This constructor exists solely to disambiguate DI container constructor resolution between the new
/// and the existing obsolete constructors; all parameters except those forwarded to the non-obsolete
/// constructor are ignored.
/// </remarks>
/// <param name="entityService">Service for accessing and managing entities within the system.</param>
/// <param name="flagProviders">A collection of providers that supply flags for entities.</param>
/// <param name="userStartNodeEntitiesService">Service for resolving user start nodes for entities.</param>
/// <param name="dataTypeService">Service for accessing and managing data types.</param>
/// <param name="treeFilterService">Service for filtering media tree entities based on user start nodes.</param>
/// <param name="appCaches">Provides access to application-level caches.</param>
/// <param name="backofficeSecurityAccessor">Accessor for backoffice security context and operations.</param>
/// <param name="mediaPresentationFactory">Factory for creating media presentation models.</param>
[Obsolete("Please use the non-obsolete constructor. Scheduled for removal in Umbraco 19.")]
[EditorBrowsable(EditorBrowsableState.Never)]
public AncestorsMediaTreeController(
IEntityService entityService,
FlagProviderCollection flagProviders,
IUserStartNodeEntitiesService userStartNodeEntitiesService,
IDataTypeService dataTypeService,
IMediaStartNodeTreeFilterService treeFilterService,
AppCaches appCaches,
IBackOfficeSecurityAccessor backofficeSecurityAccessor,
IMediaPresentationFactory mediaPresentationFactory)
: this(entityService, flagProviders, treeFilterService, mediaPresentationFactory)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="AncestorsMediaTreeController"/> class.
/// </summary>
@@ -98,7 +49,7 @@ public class AncestorsMediaTreeController : MediaTreeControllerBase
/// <param name="appCaches">Provides access to application-level caches.</param>
/// <param name="backofficeSecurityAccessor">Accessor for backoffice security context and operations.</param>
/// <param name="mediaPresentationFactory">Factory for creating media presentation models.</param>
[Obsolete("Please use the constructor accepting IMediaStartNodeTreeFilterService. Scheduled for removal in Umbraco 19.")]
[ActivatorUtilitiesConstructor]
public AncestorsMediaTreeController(
IEntityService entityService,
FlagProviderCollection flagProviders,
@@ -1,4 +1,3 @@
using System.ComponentModel;
using Asp.Versioning;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
@@ -20,54 +19,6 @@ namespace Umbraco.Cms.Api.Management.Controllers.Media.Tree;
[ApiVersion("1.0")]
public class ChildrenMediaTreeController : MediaTreeControllerBase
{
/// <summary>
/// Initializes a new instance of the <see cref="ChildrenMediaTreeController"/> class.
/// </summary>
/// <param name="entityService">Service for managing and retrieving entities within the system.</param>
/// <param name="flagProviders">A collection of providers that supply flags for entities.</param>
/// <param name="treeFilterService">Service for filtering media tree entities based on user start nodes.</param>
/// <param name="mediaPresentationFactory">Factory for creating media presentation models.</param>
[ActivatorUtilitiesConstructor]
public ChildrenMediaTreeController(
IEntityService entityService,
FlagProviderCollection flagProviders,
IMediaStartNodeTreeFilterService treeFilterService,
IMediaPresentationFactory mediaPresentationFactory)
: base(entityService, flagProviders, treeFilterService, mediaPresentationFactory)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="ChildrenMediaTreeController"/> class.
/// </summary>
/// <remarks>
/// This constructor exists solely to disambiguate DI container constructor resolution between the new
/// and the existing obsolete constructors; all parameters except those forwarded to the non-obsolete
/// constructor are ignored.
/// </remarks>
/// <param name="entityService">Service for accessing and managing entities within the system.</param>
/// <param name="flagProviders">A collection of providers that supply flags for entities.</param>
/// <param name="userStartNodeEntitiesService">Service for resolving user start nodes for entities.</param>
/// <param name="dataTypeService">Service for accessing and managing data types.</param>
/// <param name="treeFilterService">Service for filtering media tree entities based on user start nodes.</param>
/// <param name="appCaches">Provides access to application-level caches.</param>
/// <param name="backofficeSecurityAccessor">Accessor for backoffice security context and operations.</param>
/// <param name="mediaPresentationFactory">Factory for creating media presentation models.</param>
[Obsolete("Please use the non-obsolete constructor. Scheduled for removal in Umbraco 19.")]
[EditorBrowsable(EditorBrowsableState.Never)]
public ChildrenMediaTreeController(
IEntityService entityService,
FlagProviderCollection flagProviders,
IUserStartNodeEntitiesService userStartNodeEntitiesService,
IDataTypeService dataTypeService,
IMediaStartNodeTreeFilterService treeFilterService,
AppCaches appCaches,
IBackOfficeSecurityAccessor backofficeSecurityAccessor,
IMediaPresentationFactory mediaPresentationFactory)
: this(entityService, flagProviders, treeFilterService, mediaPresentationFactory)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="ChildrenMediaTreeController"/> class, responsible for handling API requests related to child media items in the media tree.
/// </summary>
@@ -99,7 +50,7 @@ public class ChildrenMediaTreeController : MediaTreeControllerBase
/// <param name="appCaches">Provides access to application-level caches for performance optimization.</param>
/// <param name="backofficeSecurityAccessor">Accessor for backoffice security context, used for authorization and user information.</param>
/// <param name="mediaPresentationFactory">Factory for creating presentation models for media entities.</param>
[Obsolete("Please use the constructor accepting IMediaStartNodeTreeFilterService. Scheduled for removal in Umbraco 19.")]
[ActivatorUtilitiesConstructor]
public ChildrenMediaTreeController(
IEntityService entityService,
FlagProviderCollection flagProviders,
@@ -1,4 +1,3 @@
using System.ComponentModel;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.DependencyInjection;
@@ -27,13 +26,10 @@ namespace Umbraco.Cms.Api.Management.Controllers.Media.Tree;
[Authorize(Policy = AuthorizationPolicies.SectionAccessForMediaTree)]
public class MediaTreeControllerBase : UserStartNodeTreeControllerBase<MediaTreeItemResponseModel>
{
private readonly AppCaches _appCaches;
private readonly IBackOfficeSecurityAccessor _backofficeSecurityAccessor;
private readonly IMediaPresentationFactory _mediaPresentationFactory;
// Only populated by the obsolete constructor path; used solely by the obsolete
// GetUserStartNodeIds / GetUserStartNodePaths overrides below.
private readonly AppCaches? _appCaches;
private readonly IBackOfficeSecurityAccessor? _backofficeSecurityAccessor;
/// <summary>
/// Initializes a new instance of the <see cref="Umbraco.Cms.Api.Management.Controllers.Media.Tree.MediaTreeControllerBase"/> class.
/// </summary>
@@ -72,73 +68,20 @@ public class MediaTreeControllerBase : UserStartNodeTreeControllerBase<MediaTree
/// <param name="appCaches">Provides access to application-level caches.</param>
/// <param name="backofficeSecurityAccessor">Accessor for backoffice security context and operations.</param>
/// <param name="mediaPresentationFactory">Factory for creating media presentation models.</param>
[Obsolete("Please use the constructor accepting IMediaStartNodeTreeFilterService. Scheduled for removal in Umbraco 19.")]
public MediaTreeControllerBase(
IEntityService entityService,
FlagProviderCollection flagProviders,
IUserStartNodeEntitiesService userStartNodeEntitiesService,
IDataTypeService dataTypeService,
AppCaches appCaches,
IBackOfficeSecurityAccessor backofficeSecurityAccessor,
IMediaPresentationFactory mediaPresentationFactory)
: base(
entityService,
flagProviders,
userStartNodeEntitiesService,
dataTypeService)
{
_mediaPresentationFactory = mediaPresentationFactory;
_appCaches = appCaches;
_backofficeSecurityAccessor = backofficeSecurityAccessor;
}
/// <summary>
/// Initializes a new instance of the <see cref="MediaTreeControllerBase"/> class.
/// </summary>
/// <param name="entityService">Service for accessing and managing entities within the system.</param>
/// <param name="flagProviders">A collection of providers that supply flags for entities.</param>
/// <param name="treeFilterService">Service for filtering media tree entities based on user start nodes.</param>
/// <param name="mediaPresentationFactory">Factory for creating media presentation models.</param>
[ActivatorUtilitiesConstructor]
public MediaTreeControllerBase(
IEntityService entityService,
FlagProviderCollection flagProviders,
IMediaStartNodeTreeFilterService treeFilterService,
IMediaPresentationFactory mediaPresentationFactory)
: base(entityService, flagProviders, treeFilterService) =>
_mediaPresentationFactory = mediaPresentationFactory;
/// <summary>
/// Initializes a new instance of the <see cref="MediaTreeControllerBase"/> class.
/// </summary>
/// <remarks>
/// This constructor is a parameter superset of the new and existing obsolete constructors. It exists
/// solely because <see cref="ActivatorUtilitiesConstructorAttribute"/> is not honoured by the DI
/// <c>CallSiteFactory</c> at <c>ServiceProvider</c> <c>ValidateOnBuild</c> time, which requires an
/// unambiguous single best-match constructor; all parameters except those forwarded to the non-obsolete
/// constructor are ignored.
/// </remarks>
/// <param name="entityService">Service for accessing and managing entities within the system.</param>
/// <param name="flagProviders">A collection of providers that supply flags for entities.</param>
/// <param name="userStartNodeEntitiesService">Service for resolving user start nodes for entities.</param>
/// <param name="dataTypeService">Service for accessing and managing data types.</param>
/// <param name="treeFilterService">Service for filtering media tree entities based on user start nodes.</param>
/// <param name="appCaches">Provides access to application-level caches.</param>
/// <param name="backofficeSecurityAccessor">Accessor for backoffice security context and operations.</param>
/// <param name="mediaPresentationFactory">Factory for creating media presentation models.</param>
[Obsolete("Please use the non-obsolete constructor. Scheduled for removal in Umbraco 19.")]
[EditorBrowsable(EditorBrowsableState.Never)]
public MediaTreeControllerBase(
IEntityService entityService,
FlagProviderCollection flagProviders,
IUserStartNodeEntitiesService userStartNodeEntitiesService,
IDataTypeService dataTypeService,
IMediaStartNodeTreeFilterService treeFilterService,
AppCaches appCaches,
IBackOfficeSecurityAccessor backofficeSecurityAccessor,
IMediaPresentationFactory mediaPresentationFactory)
: this(entityService, flagProviders, treeFilterService, mediaPresentationFactory)
: base(entityService, flagProviders, userStartNodeEntitiesService, dataTypeService)
{
_appCaches = appCaches;
_backofficeSecurityAccessor = backofficeSecurityAccessor;
_mediaPresentationFactory = mediaPresentationFactory;
}
protected override UmbracoObjectTypes ItemObjectType => UmbracoObjectTypes.Media;
@@ -162,23 +105,17 @@ public class MediaTreeControllerBase : UserStartNodeTreeControllerBase<MediaTree
return responseModel;
}
// Only invoked via the CallbackStartNodeTreeFilterService wired up by the obsolete
// UserStartNodeTreeControllerBase constructor. The non-obsolete constructor path
// routes start node resolution through IMediaStartNodeTreeFilterService and
// never calls these overrides; hence the null-forgiving operator on _appCaches.
[Obsolete("No longer used. Register a custom IMediaStartNodeTreeFilterService instead. Scheduled for removal in Umbraco 19.")]
protected override int[] GetUserStartNodeIds()
=> _backofficeSecurityAccessor?
=> _backofficeSecurityAccessor
.BackOfficeSecurity?
.CurrentUser?
.CalculateMediaStartNodeIds(EntityService, _appCaches!)
?? [];
.CalculateMediaStartNodeIds(EntityService, _appCaches)
?? Array.Empty<int>();
[Obsolete("No longer used. Register a custom IMediaStartNodeTreeFilterService instead. Scheduled for removal in Umbraco 19.")]
protected override string[] GetUserStartNodePaths()
=> _backofficeSecurityAccessor?
=> _backofficeSecurityAccessor
.BackOfficeSecurity?
.CurrentUser?
.GetMediaStartNodePaths(EntityService, _appCaches!)
?? [];
.GetMediaStartNodePaths(EntityService, _appCaches)
?? Array.Empty<string>();
}
@@ -1,4 +1,3 @@
using System.ComponentModel;
using Asp.Versioning;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
@@ -20,54 +19,6 @@ namespace Umbraco.Cms.Api.Management.Controllers.Media.Tree;
[ApiVersion("1.0")]
public class RootMediaTreeController : MediaTreeControllerBase
{
/// <summary>
/// Initializes a new instance of the <see cref="RootMediaTreeController"/> class.
/// </summary>
/// <param name="entityService">Service for managing and retrieving entities in the system.</param>
/// <param name="flagProviders">A collection of providers that supply flags for entities.</param>
/// <param name="treeFilterService">Service for filtering media tree entities based on user start nodes.</param>
/// <param name="mediaPresentationFactory">Factory for creating media presentation models.</param>
[ActivatorUtilitiesConstructor]
public RootMediaTreeController(
IEntityService entityService,
FlagProviderCollection flagProviders,
IMediaStartNodeTreeFilterService treeFilterService,
IMediaPresentationFactory mediaPresentationFactory)
: base(entityService, flagProviders, treeFilterService, mediaPresentationFactory)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="RootMediaTreeController"/> class.
/// </summary>
/// <remarks>
/// This constructor exists solely to disambiguate DI container constructor resolution between the new
/// and the existing obsolete constructors; all parameters except those forwarded to the non-obsolete
/// constructor are ignored.
/// </remarks>
/// <param name="entityService">Service for accessing and managing entities within the system.</param>
/// <param name="flagProviders">A collection of providers that supply flags for entities.</param>
/// <param name="userStartNodeEntitiesService">Service for resolving user start nodes for entities.</param>
/// <param name="dataTypeService">Service for accessing and managing data types.</param>
/// <param name="treeFilterService">Service for filtering media tree entities based on user start nodes.</param>
/// <param name="appCaches">Provides access to application-level caches.</param>
/// <param name="backofficeSecurityAccessor">Accessor for backoffice security context and operations.</param>
/// <param name="mediaPresentationFactory">Factory for creating media presentation models.</param>
[Obsolete("Please use the non-obsolete constructor. Scheduled for removal in Umbraco 19.")]
[EditorBrowsable(EditorBrowsableState.Never)]
public RootMediaTreeController(
IEntityService entityService,
FlagProviderCollection flagProviders,
IUserStartNodeEntitiesService userStartNodeEntitiesService,
IDataTypeService dataTypeService,
IMediaStartNodeTreeFilterService treeFilterService,
AppCaches appCaches,
IBackOfficeSecurityAccessor backofficeSecurityAccessor,
IMediaPresentationFactory mediaPresentationFactory)
: this(entityService, flagProviders, treeFilterService, mediaPresentationFactory)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="RootMediaTreeController"/> class, which manages the root of the media tree in the Umbraco backoffice API.
/// </summary>
@@ -99,7 +50,7 @@ public class RootMediaTreeController : MediaTreeControllerBase
/// <param name="appCaches">Provides access to application-level caches.</param>
/// <param name="backofficeSecurityAccessor">Accessor for backoffice security context and operations.</param>
/// <param name="mediaPresentationFactory">Factory for creating media presentation models.</param>
[Obsolete("Please use the constructor accepting IMediaStartNodeTreeFilterService. Scheduled for removal in Umbraco 19.")]
[ActivatorUtilitiesConstructor]
public RootMediaTreeController(
IEntityService entityService,
FlagProviderCollection flagProviders,
@@ -1,4 +1,3 @@
using System.ComponentModel;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.DependencyInjection;
@@ -19,54 +18,6 @@ namespace Umbraco.Cms.Api.Management.Controllers.Media.Tree;
/// </summary>
public class SiblingsMediaTreeController : MediaTreeControllerBase
{
/// <summary>
/// Initializes a new instance of the <see cref="SiblingsMediaTreeController"/> class.
/// </summary>
/// <param name="entityService">Service for accessing and managing entities within the system.</param>
/// <param name="flagProviders">A collection of providers that supply flags for entities.</param>
/// <param name="treeFilterService">Service for filtering media tree entities based on user start nodes.</param>
/// <param name="mediaPresentationFactory">Factory for creating media presentation models.</param>
[ActivatorUtilitiesConstructor]
public SiblingsMediaTreeController(
IEntityService entityService,
FlagProviderCollection flagProviders,
IMediaStartNodeTreeFilterService treeFilterService,
IMediaPresentationFactory mediaPresentationFactory)
: base(entityService, flagProviders, treeFilterService, mediaPresentationFactory)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="SiblingsMediaTreeController"/> class.
/// </summary>
/// <remarks>
/// This constructor exists solely to disambiguate DI container constructor resolution between the new
/// and the existing obsolete constructors; all parameters except those forwarded to the non-obsolete
/// constructor are ignored.
/// </remarks>
/// <param name="entityService">Service for accessing and managing entities within the system.</param>
/// <param name="flagProviders">A collection of providers that supply flags for entities.</param>
/// <param name="userStartNodeEntitiesService">Service for resolving user start nodes for entities.</param>
/// <param name="dataTypeService">Service for accessing and managing data types.</param>
/// <param name="treeFilterService">Service for filtering media tree entities based on user start nodes.</param>
/// <param name="appCaches">Provides access to application-level caches.</param>
/// <param name="backofficeSecurityAccessor">Accessor for backoffice security context and operations.</param>
/// <param name="mediaPresentationFactory">Factory for creating media presentation models.</param>
[Obsolete("Please use the non-obsolete constructor. Scheduled for removal in Umbraco 19.")]
[EditorBrowsable(EditorBrowsableState.Never)]
public SiblingsMediaTreeController(
IEntityService entityService,
FlagProviderCollection flagProviders,
IUserStartNodeEntitiesService userStartNodeEntitiesService,
IDataTypeService dataTypeService,
IMediaStartNodeTreeFilterService treeFilterService,
AppCaches appCaches,
IBackOfficeSecurityAccessor backofficeSecurityAccessor,
IMediaPresentationFactory mediaPresentationFactory)
: this(entityService, flagProviders, treeFilterService, mediaPresentationFactory)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="SiblingsMediaTreeController"/> class, responsible for handling API requests related to sibling media items in the media tree.
/// </summary>
@@ -98,7 +49,7 @@ public class SiblingsMediaTreeController : MediaTreeControllerBase
/// <param name="appCaches">Provides access to application-level caches for performance optimization.</param>
/// <param name="backofficeSecurityAccessor">Accessor for back office security context, used for authorization and user information.</param>
/// <param name="mediaPresentationFactory">Factory for creating media presentation models for API responses.</param>
[Obsolete("Please use the constructor accepting IMediaStartNodeTreeFilterService. Scheduled for removal in Umbraco 19.")]
[ActivatorUtilitiesConstructor]
public SiblingsMediaTreeController(
IEntityService entityService,
FlagProviderCollection flagProviders,
@@ -54,14 +54,11 @@ public class SearchMediaTypeItemController : MediaTypeItemControllerBase
return Task.FromResult<IActionResult>(Ok(new PagedModel<MediaTypeItemResponseModel> { Total = searchResult.Total }));
}
Guid[] keys = searchResult.Items.Select(item => item.Key).ToArray();
IEnumerable<IMediaType> mediaTypes = _mediaTypeService.GetMany(keys.EmptyNull());
IEnumerable<IMediaType> orderedMediaTypes = OrderByRequestedIds(mediaTypes, keys);
IEnumerable<IMediaType> mediaTypes = _mediaTypeService.GetMany(searchResult.Items.Select(item => item.Key).ToArray().EmptyNull());
var result = new PagedModel<MediaTypeItemResponseModel>
{
Items = _mapper.MapEnumerable<IMediaType, MediaTypeItemResponseModel>(orderedMediaTypes),
Total = searchResult.Total,
Items = _mapper.MapEnumerable<IMediaType, MediaTypeItemResponseModel>(mediaTypes),
Total = searchResult.Total
};
return Task.FromResult<IActionResult>(Ok(result));
@@ -1,11 +1,9 @@
using Asp.Versioning;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.DependencyInjection;
using Umbraco.Cms.Api.Management.Factories;
using Umbraco.Cms.Api.Management.Services;
using Umbraco.Cms.Api.Management.ViewModels.Member;
using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Security;
using Umbraco.Cms.Core.Services;
@@ -18,43 +16,24 @@ namespace Umbraco.Cms.Api.Management.Controllers.Member;
[ApiVersion("1.0")]
public class ByKeyMemberController : MemberControllerBase
{
private readonly IMemberEditingService _memberEditingService;
private readonly IMemberPresentationFactory _memberPresentationFactory;
private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor;
private readonly IMemberPresentationService _memberPresentationService;
// TODO (V19): Remove the unnecessary parameters provided to the constructor.
/// <summary>
/// Initializes a new instance of the <see cref="ByKeyMemberController"/> class.
/// Initializes a new instance of the <see cref="ByKeyMemberController"/> class, which handles member management operations by member key.
/// </summary>
/// <param name="memberEditingService">Service used to perform editing operations on members.</param>
/// <param name="memberPresentationFactory">Factory for creating member presentation models.</param>
/// <param name="backOfficeSecurityAccessor">Accessor for back office security context.</param>
/// <param name="memberPresentationService">Service for resolving members across both content and external stores.</param>
[ActivatorUtilitiesConstructor]
public ByKeyMemberController(
IMemberEditingService memberEditingService,
IMemberPresentationFactory memberPresentationFactory,
IBackOfficeSecurityAccessor backOfficeSecurityAccessor,
IMemberPresentationService memberPresentationService)
{
_backOfficeSecurityAccessor = backOfficeSecurityAccessor;
_memberPresentationService = memberPresentationService;
}
/// <summary>
/// Initializes a new instance of the <see cref="ByKeyMemberController"/> class.
/// </summary>
[Obsolete("Please use the constructor with all parameters. Scheduled for removal in Umbraco 19.")]
public ByKeyMemberController(
IMemberEditingService memberEditingService,
IMemberPresentationFactory memberPresentationFactory,
IBackOfficeSecurityAccessor backOfficeSecurityAccessor)
: this(
memberEditingService,
memberPresentationFactory,
backOfficeSecurityAccessor,
StaticServiceProvider.Instance.GetRequiredService<IMemberPresentationService>())
{
_memberEditingService = memberEditingService;
_memberPresentationFactory = memberPresentationFactory;
_backOfficeSecurityAccessor = backOfficeSecurityAccessor;
}
/// <summary>
@@ -73,7 +52,13 @@ public class ByKeyMemberController : MemberControllerBase
[EndpointDescription("Gets a member identified by the provided Id.")]
public async Task<IActionResult> ByKey(CancellationToken cancellationToken, Guid id)
{
MemberResponseModel? model = await _memberPresentationService.CreateResponseModelByKeyAsync(id, CurrentUser(_backOfficeSecurityAccessor));
return model is not null ? Ok(model) : MemberNotFound();
IMember? member = await _memberEditingService.GetAsync(id);
if (member == null)
{
return MemberNotFound();
}
MemberResponseModel model = await _memberPresentationFactory.CreateResponseModelAsync(member, CurrentUser(_backOfficeSecurityAccessor));
return Ok(model);
}
}
@@ -19,13 +19,11 @@ public class DeleteMemberController : MemberControllerBase
private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor;
/// <summary>
/// Initializes a new instance of the <see cref="DeleteMemberController"/> class.
/// Initializes a new instance of the <see cref="DeleteMemberController"/> class, which handles member deletion operations.
/// </summary>
/// <param name="memberEditingService">Service used to perform member editing and deletion operations.</param>
/// <param name="backOfficeSecurityAccessor">Accessor for back office security context and authorization.</param>
public DeleteMemberController(
IMemberEditingService memberEditingService,
IBackOfficeSecurityAccessor backOfficeSecurityAccessor)
public DeleteMemberController(IMemberEditingService memberEditingService, IBackOfficeSecurityAccessor backOfficeSecurityAccessor)
{
_memberEditingService = memberEditingService;
_backOfficeSecurityAccessor = backOfficeSecurityAccessor;
@@ -1,15 +1,10 @@
// Copyright (c) Umbraco.
// See LICENSE for more details.
using Asp.Versioning;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.DependencyInjection;
using Umbraco.Cms.Api.Common.ViewModels.Pagination;
using Umbraco.Cms.Api.Management.Factories;
using Umbraco.Cms.Api.Management.ViewModels.Member;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.Membership;
using Umbraco.Cms.Core.Security;
@@ -24,48 +19,40 @@ namespace Umbraco.Cms.Api.Management.Controllers.Member.Filter;
[ApiVersion("1.0")]
public class FilterMemberFilterController : MemberFilterControllerBase
{
private readonly IMemberFilterService _memberFilterService;
private readonly IMemberService _memberService;
private readonly IMemberPresentationFactory _memberPresentationFactory;
private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor;
/// <summary>
/// Initializes a new instance of the <see cref="FilterMemberFilterController"/> class.
/// </summary>
/// <param name="memberService">Service used for member management operations (unused, retained for DI compatibility).</param>
/// <param name="memberService">Service used for member management operations.</param>
/// <param name="memberPresentationFactory">Factory responsible for creating member presentation models.</param>
/// <param name="backOfficeSecurityAccessor">Accessor for back office security context (unused, retained for DI compatibility).</param>
/// <param name="memberFilterService">Service for combined member filtering across content and external stores.</param>
// TODO (V19): Remove unused parameters which are only here to avoid ambiguous constructor errors.
[ActivatorUtilitiesConstructor]
public FilterMemberFilterController(
IMemberService memberService,
IMemberPresentationFactory memberPresentationFactory,
IBackOfficeSecurityAccessor backOfficeSecurityAccessor,
IMemberFilterService memberFilterService)
{
_memberFilterService = memberFilterService;
_memberPresentationFactory = memberPresentationFactory;
}
/// <summary>
/// Initializes a new instance of the <see cref="FilterMemberFilterController"/> class.
/// </summary>
[Obsolete("Please use the constructor with all parameters. Scheduled for removal in Umbraco 19.")]
/// <param name="backOfficeSecurityAccessor">Accessor for back office security context and authentication.</param>
public FilterMemberFilterController(
IMemberService memberService,
IMemberPresentationFactory memberPresentationFactory,
IBackOfficeSecurityAccessor backOfficeSecurityAccessor)
: this(
memberService,
memberPresentationFactory,
backOfficeSecurityAccessor,
StaticServiceProvider.Instance.GetRequiredService<IMemberFilterService>())
{
_memberService = memberService;
_memberPresentationFactory = memberPresentationFactory;
_backOfficeSecurityAccessor = backOfficeSecurityAccessor;
}
/// <summary>
/// Retrieves a paged, filtered collection of members based on the specified criteria.
/// Returns both content-based and external-only members in a unified, correctly paginated result.
/// </summary>
/// <param name="cancellationToken">A token to monitor for cancellation requests.</param>
/// <param name="memberTypeId">An optional member type identifier to filter the results.</param>
/// <param name="memberGroupName">An optional member group name to filter the results.</param>
/// <param name="isApproved">An optional value to filter by member approval status.</param>
/// <param name="isLockedOut">An optional value to filter by member lockout status.</param>
/// <param name="orderBy">The field by which to order the results. The default is <c>"username"</c>.</param>
/// <param name="orderDirection">The direction in which to order the results. The default is <see cref="Direction.Ascending"/>.</param>
/// <param name="filter">An optional filter string to search for members.</param>
/// <param name="skip">The number of items to skip for pagination. The default is 0.</param>
/// <param name="take">The number of items to return for pagination. The default is 100.</param>
/// <returns>A task representing the asynchronous operation. The task result contains an <see cref="IActionResult"/> with a <see cref="PagedViewModel{MemberResponseModel}"/> representing the filtered members.</returns>
[HttpGet]
[MapToApiVersion("1.0")]
[ProducesResponseType(typeof(PagedViewModel<MemberResponseModel>), StatusCodes.Status200OK)]
@@ -84,7 +71,7 @@ public class FilterMemberFilterController : MemberFilterControllerBase
int skip = 0,
int take = 100)
{
var memberFilter = new MemberFilter
var memberFilter = new MemberFilter()
{
MemberTypeId = memberTypeId,
MemberGroupName = memberGroupName,
@@ -93,14 +80,14 @@ public class FilterMemberFilterController : MemberFilterControllerBase
Filter = filter,
};
PagedModel<MemberFilterItem> result = await _memberFilterService.FilterAsync(memberFilter, orderBy, orderDirection, skip, take);
PagedModel<IMember> members = await _memberService.FilterAsync(memberFilter, orderBy, orderDirection, skip, take);
var responseModels = result.Items.Select(_memberPresentationFactory.CreateFilterItemResponseModel).ToList();
return Ok(new PagedViewModel<MemberResponseModel>
var pageViewModel = new PagedViewModel<MemberResponseModel>
{
Items = responseModels,
Total = result.Total,
});
Items = await _memberPresentationFactory.CreateMultipleAsync(members.Items, CurrentUser(_backOfficeSecurityAccessor)),
Total = members.Total,
};
return Ok(pageViewModel);
}
}
@@ -1,11 +1,11 @@
using Asp.Versioning;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.DependencyInjection;
using Umbraco.Cms.Api.Management.Factories;
using Umbraco.Cms.Api.Management.Services;
using Umbraco.Cms.Api.Management.ViewModels.Member.Item;
using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Core.Mapping;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.Entities;
using Umbraco.Cms.Core.Services;
namespace Umbraco.Cms.Api.Management.Controllers.Member.Item;
@@ -17,37 +17,18 @@ namespace Umbraco.Cms.Api.Management.Controllers.Member.Item;
[ApiVersion("1.0")]
public class ItemMemberItemController : MemberItemControllerBase
{
private readonly IMemberPresentationService _memberPresentationService;
// TODO (V19): Remove the unnecessary parameters provided to the constructor.
private readonly IEntityService _entityService;
private readonly IMemberPresentationFactory _memberPresentationFactory;
/// <summary>
/// Initializes a new instance of the <see cref="ItemMemberItemController"/> class.
/// Initializes a new instance of the <see cref="ItemMemberItemController"/> class, which manages member item operations in the API.
/// </summary>
/// <param name="entityService">Service used for entity operations and retrieval.</param>
/// <param name="memberPresentationFactory">Factory responsible for creating member presentation models.</param>
/// <param name="memberPresentationService">Service for resolving members across both content and external stores.</param>
[ActivatorUtilitiesConstructor]
public ItemMemberItemController(
IEntityService entityService,
IMemberPresentationFactory memberPresentationFactory,
IMemberPresentationService memberPresentationService)
{
_memberPresentationService = memberPresentationService;
}
/// <summary>
/// Initializes a new instance of the <see cref="ItemMemberItemController"/> class.
/// </summary>
[Obsolete("Please use the constructor with all parameters. Scheduled for removal in Umbraco 19.")]
public ItemMemberItemController(
IEntityService entityService,
IMemberPresentationFactory memberPresentationFactory)
: this(
entityService,
memberPresentationFactory,
StaticServiceProvider.Instance.GetRequiredService<IMemberPresentationService>())
public ItemMemberItemController(IEntityService entityService, IMemberPresentationFactory memberPresentationFactory)
{
_entityService = entityService;
_memberPresentationFactory = memberPresentationFactory;
}
[HttpGet]
@@ -55,16 +36,20 @@ public class ItemMemberItemController : MemberItemControllerBase
[ProducesResponseType(typeof(IEnumerable<MemberItemResponseModel>), StatusCodes.Status200OK)]
[EndpointSummary("Gets a collection of member items.")]
[EndpointDescription("Gets a collection of member items identified by the provided Ids.")]
public async Task<IActionResult> Item(
public Task<IActionResult> Item(
CancellationToken cancellationToken,
[FromQuery(Name = "id")] HashSet<Guid> ids)
{
if (ids.Count is 0)
{
return Ok(Enumerable.Empty<MemberItemResponseModel>());
return Task.FromResult<IActionResult>(Ok(Enumerable.Empty<MemberItemResponseModel>()));
}
IEnumerable<MemberItemResponseModel> responseModels = await _memberPresentationService.CreateItemResponseModelsAsync(ids);
return Ok(responseModels);
IEnumerable<IMemberEntitySlim> members = _entityService
.GetAll(UmbracoObjectTypes.Member, ids.ToArray())
.OfType<IMemberEntitySlim>();
IEnumerable<MemberItemResponseModel> responseModels = members.Select(_memberPresentationFactory.CreateItemResponseModel);
return Task.FromResult<IActionResult>(Ok(responseModels));
}
}
@@ -96,15 +96,6 @@ public class MemberControllerBase : ContentControllerBase
where TContentModelBase : ContentModelBase<MemberValueModel, MemberVariantRequestModel>
=> ContentEditingOperationStatusResult<TContentModelBase, MemberValueModel, MemberVariantRequestModel>(status, requestModel, validationResult);
/// <summary>
/// Returns a 400 Bad Request indicating that external-only members cannot be modified through the Management API.
/// </summary>
protected IActionResult ExternalMemberCannotBeModified()
=> BadRequest(new ProblemDetailsBuilder()
.WithTitle("External member cannot be modified")
.WithDetail("This member is managed by an external provider. Content operations such as create, update, and property editing are not available for external-only members.")
.Build());
private IActionResult MemberNotFound(ProblemDetailsBuilder problemDetailsBuilder) => NotFound(problemDetailsBuilder
.WithTitle("The requested member could not be found")
.Build());
@@ -1,13 +1,10 @@
using Asp.Versioning;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.DependencyInjection;
using Umbraco.Cms.Api.Common.ViewModels.Pagination;
using Umbraco.Cms.Api.Management.Factories;
using Umbraco.Cms.Api.Management.Services;
using Umbraco.Cms.Api.Management.ViewModels.TrackedReferences;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Services.OperationStatus;
@@ -20,39 +17,20 @@ namespace Umbraco.Cms.Api.Management.Controllers.Member.References;
[ApiVersion("1.0")]
public class ReferencedByMemberController : MemberControllerBase
{
private readonly ITrackedReferencesService _trackedReferencesService;
private readonly IRelationTypePresentationFactory _relationTypePresentationFactory;
private readonly IMemberReferenceService _memberReferenceService;
// TODO (V19): Remove the unnecessary parameters provided to the constructor.
/// <summary>
/// Initializes a new instance of the <see cref="ReferencedByMemberController"/> class.
/// </summary>
/// <param name="trackedReferencesService">An implementation of <see cref="ITrackedReferencesService"/> used to manage tracked references.</param>
/// <param name="relationTypePresentationFactory">An implementation of <see cref="IRelationTypePresentationFactory"/> used to create relation type presentations.</param>
/// <param name="memberReferenceService">Service for retrieving paged references to a member.</param>
[ActivatorUtilitiesConstructor]
public ReferencedByMemberController(
ITrackedReferencesService trackedReferencesService,
IRelationTypePresentationFactory relationTypePresentationFactory,
IMemberReferenceService memberReferenceService)
{
_relationTypePresentationFactory = relationTypePresentationFactory;
_memberReferenceService = memberReferenceService;
}
/// <summary>
/// Initializes a new instance of the <see cref="ReferencedByMemberController"/> class.
/// </summary>
[Obsolete("Please use the constructor with all parameters. Scheduled for removal in Umbraco 19.")]
public ReferencedByMemberController(
ITrackedReferencesService trackedReferencesService,
IRelationTypePresentationFactory relationTypePresentationFactory)
: this(
trackedReferencesService,
relationTypePresentationFactory,
StaticServiceProvider.Instance.GetRequiredService<IMemberReferenceService>())
{
_trackedReferencesService = trackedReferencesService;
_relationTypePresentationFactory = relationTypePresentationFactory;
}
/// <summary>
@@ -74,12 +52,12 @@ public class ReferencedByMemberController : MemberControllerBase
int skip = 0,
int take = 20)
{
Attempt<PagedModel<RelationItemModel>, GetReferencesOperationStatus> result = await _memberReferenceService.GetPagedReferencesAsync(id, skip, take);
PagedModel<RelationItemModel> relationItems = await _trackedReferencesService.GetPagedRelationsForItemAsync(id, skip, take, true);
var pagedViewModel = new PagedViewModel<IReferenceResponseModel>
{
Total = result.Result.Total,
Items = await _relationTypePresentationFactory.CreateReferenceResponseModelsAsync(result.Result.Items),
Total = relationItems.Total,
Items = await _relationTypePresentationFactory.CreateReferenceResponseModelsAsync(relationItems.Items),
};
return pagedViewModel;
@@ -109,17 +87,17 @@ public class ReferencedByMemberController : MemberControllerBase
int skip = 0,
int take = 20)
{
Attempt<PagedModel<RelationItemModel>, GetReferencesOperationStatus> result = await _memberReferenceService.GetPagedReferencesAsync(id, skip, take);
Attempt<PagedModel<RelationItemModel>, GetReferencesOperationStatus> relationItemsAttempt = await _trackedReferencesService.GetPagedRelationsForItemAsync(id, UmbracoObjectTypes.Member, skip, take, true);
if (result.Success is false)
if (relationItemsAttempt.Success is false)
{
return GetReferencesOperationStatusResult(result.Status);
return GetReferencesOperationStatusResult(relationItemsAttempt.Status);
}
var pagedViewModel = new PagedViewModel<IReferenceResponseModel>
{
Total = result.Result.Total,
Items = await _relationTypePresentationFactory.CreateReferenceResponseModelsAsync(result.Result.Items),
Total = relationItemsAttempt.Result.Total,
Items = await _relationTypePresentationFactory.CreateReferenceResponseModelsAsync(relationItemsAttempt.Result.Items),
};
return Ok(pagedViewModel);
@@ -22,7 +22,7 @@ public class UpdateMemberController : MemberControllerBase
private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor;
/// <summary>
/// Initializes a new instance of the <see cref="UpdateMemberController"/> class.
/// Initializes a new instance of the <see cref="UpdateMemberController"/> class, responsible for handling member update operations in the management API.
/// </summary>
/// <param name="memberEditingService">Service used to perform member editing operations.</param>
/// <param name="memberEditingPresentationFactory">Factory for creating presentation models related to member editing.</param>
@@ -49,13 +49,6 @@ public class UpdateMemberController : MemberControllerBase
Guid id,
UpdateMemberRequestModel updateRequestModel)
{
// External-only members cannot be updated through this endpoint.
// Their identity data is managed by the external provider.
if (await _memberEditingService.IsExternalMemberAsync(id))
{
return ExternalMemberCannotBeModified();
}
MemberUpdateModel model = _memberEditingPresentationFactory.MapUpdateModel(updateRequestModel);
Attempt<MemberUpdateResult, MemberEditingStatus> result = await _memberEditingService.UpdateAsync(id, model, CurrentUser(_backOfficeSecurityAccessor));
@@ -20,7 +20,7 @@ public class ValidateUpdateMemberController : MemberControllerBase
private readonly IMemberEditingPresentationFactory _memberEditingPresentationFactory;
/// <summary>
/// Initializes a new instance of the <see cref="ValidateUpdateMemberController"/> class.
/// Initializes a new instance of the <see cref="Umbraco.Cms.Api.Management.Controllers.Member.ValidateUpdateMemberController"/> class.
/// </summary>
/// <param name="memberEditingService">The <see cref="IMemberEditingService"/> used for member editing operations.</param>
/// <param name="memberEditingPresentationFactory">The <see cref="IMemberEditingPresentationFactory"/> used to create member editing presentations.</param>
@@ -44,12 +44,6 @@ public class ValidateUpdateMemberController : MemberControllerBase
Guid id,
UpdateMemberRequestModel requestModel)
{
// External-only members cannot be updated through this endpoint.
if (await _memberEditingService.IsExternalMemberAsync(id))
{
return ExternalMemberCannotBeModified();
}
MemberUpdateModel model = _memberEditingPresentationFactory.MapUpdateModel(requestModel);
Attempt<ContentValidationResult, ContentEditingOperationStatus> result = await _memberEditingService.ValidateUpdateAsync(id, model);
@@ -1,11 +1,11 @@
using Asp.Versioning;
using J2N.Collections.Generic.Extensions;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.DependencyInjection;
using Umbraco.Cms.Api.Management.ViewModels.MemberGroup.Item;
using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Core.Mapping;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.Entities;
using Umbraco.Cms.Core.Services;
namespace Umbraco.Cms.Api.Management.Controllers.MemberGroup.Item;
@@ -16,30 +16,18 @@ namespace Umbraco.Cms.Api.Management.Controllers.MemberGroup.Item;
[ApiVersion("1.0")]
public class ItemMemberGroupItemController : MemberGroupItemControllerBase
{
private readonly IEntityService _entityService;
private readonly IUmbracoMapper _mapper;
private readonly IMemberGroupService _memberGroupService;
// TODO (V19): When the obsolete constructor is removed, also remove the unused dependency on IEntityService.
/// <summary>
/// Initializes a new instance of the <see cref="ItemMemberGroupItemController"/> class.
/// Initializes a new instance of the <see cref="Umbraco.Cms.Api.Management.Controllers.MemberGroup.Item.ItemMemberGroupItemController"/> class, providing services for managing member group items.
/// </summary>
/// <param name="entityService">The service used to interact with entities in the Umbraco CMS.</param>
/// <param name="mapper">The mapper used for mapping Umbraco objects.</param>
/// <param name="memberGroupService">The service used to look up member groups.</param>
[ActivatorUtilitiesConstructor]
public ItemMemberGroupItemController(IEntityService entityService, IUmbracoMapper mapper, IMemberGroupService memberGroupService)
{
_mapper = mapper;
_memberGroupService = memberGroupService;
}
[Obsolete("Please use the constructor with all parameters. Scheduled for removal in Umbraco 19.")]
public ItemMemberGroupItemController(IEntityService entityService, IUmbracoMapper mapper)
: this(
entityService,
mapper,
StaticServiceProvider.Instance.GetRequiredService<IMemberGroupService>())
{
_entityService = entityService;
_mapper = mapper;
}
[HttpGet]
@@ -47,19 +35,17 @@ public class ItemMemberGroupItemController : MemberGroupItemControllerBase
[ProducesResponseType(typeof(IEnumerable<MemberGroupItemResponseModel>), StatusCodes.Status200OK)]
[EndpointSummary("Gets a collection of member group items.")]
[EndpointDescription("Gets a collection of member group items identified by the provided Ids.")]
public async Task<IActionResult> Item(
public Task<IActionResult> Item(
CancellationToken cancellationToken,
[FromQuery(Name = "id")] HashSet<Guid> ids)
{
if (ids.Count is 0)
{
return Ok(Enumerable.Empty<MemberGroupItemResponseModel>());
return Task.FromResult<IActionResult>(Ok(Enumerable.Empty<MemberGroupItemResponseModel>()));
}
// Resolve via IMemberGroupService so custom implementations are honoured, rather than
// going directly to the entity/repository layer.
IEnumerable<IMemberGroup> memberGroups = await _memberGroupService.GetAsync(ids);
List<MemberGroupItemResponseModel> responseModel = _mapper.MapEnumerable<IMemberGroup, MemberGroupItemResponseModel>(memberGroups);
return Ok(responseModel);
IEnumerable<IEntitySlim> memberGroups = _entityService.GetAll(UmbracoObjectTypes.MemberGroup, ids.ToArray());
List<MemberGroupItemResponseModel> responseModel = _mapper.MapEnumerable<IEntitySlim, MemberGroupItemResponseModel>(memberGroups);
return Task.FromResult<IActionResult>(Ok(responseModel));
}
}
@@ -32,14 +32,6 @@ public class SearchMemberTypeItemController : MemberTypeItemControllerBase
_mapper = mapper;
}
/// <summary>
/// Searches for member type items matching the specified query, with support for pagination.
/// </summary>
/// <param name="cancellationToken">A token to monitor for cancellation requests.</param>
/// <param name="query">The search query used to filter member type items.</param>
/// <param name="skip">The number of items to skip before starting to collect the result set (used for pagination).</param>
/// <param name="take">The maximum number of items to return in the result set (used for pagination).</param>
/// <returns>A task representing the asynchronous operation. The task result contains an <see cref="IActionResult"/> with a <see cref="PagedModel{MemberTypeItemResponseModel}"/> containing the search results.</returns>
[HttpGet("search")]
[MapToApiVersion("1.0")]
[ProducesResponseType(typeof(PagedModel<MemberTypeItemResponseModel>), StatusCodes.Status200OK)]
@@ -53,14 +45,11 @@ public class SearchMemberTypeItemController : MemberTypeItemControllerBase
return Task.FromResult<IActionResult>(Ok(new PagedModel<MemberTypeItemResponseModel> { Total = searchResult.Total }));
}
Guid[] keys = searchResult.Items.Select(item => item.Key).ToArray();
IEnumerable<IMemberType> memberTypes = _memberTypeService.GetMany(keys);
IEnumerable<IMemberType> orderedMemberTypes = OrderByRequestedIds(memberTypes, keys);
IEnumerable<IMemberType> memberTypes = _memberTypeService.GetMany(searchResult.Items.Select(item => item.Key).ToArray());
var result = new PagedModel<MemberTypeItemResponseModel>
{
Items = _mapper.MapEnumerable<IMemberType, MemberTypeItemResponseModel>(orderedMemberTypes),
Total = searchResult.Total,
Items = _mapper.MapEnumerable<IMemberType, MemberTypeItemResponseModel>(memberTypes),
Total = searchResult.Total
};
return Task.FromResult<IActionResult>(Ok(result));
@@ -8,38 +8,67 @@ using Umbraco.Cms.Core.Security;
namespace Umbraco.Cms.Api.Management.Controllers.RedirectUrlManagement;
/// <summary>
/// Controller for setting the redirect URL tracking status. Retained for backwards compatibility only;
/// the endpoint no longer modifies any configuration.
/// Controller for setting the redirect URL tracking status.
/// </summary>
[ApiVersion("1.0")]
[Obsolete("This controller is deprecated and no longer modifies the configuration. Set the Umbraco:CMS:WebRouting:DisableRedirectUrlTracking configuration key instead. Scheduled for removal in Umbraco 19.")]
public class SetStatusRedirectUrlManagementController : RedirectUrlManagementControllerBase
{
private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor;
private readonly IConfigManipulator _configManipulator;
/// <summary>
/// Initializes a new instance of the <see cref="SetStatusRedirectUrlManagementController"/> class.
/// </summary>
/// <param name="backOfficeSecurityAccessor">Ignored. Retained for binary compatibility.</param>
/// <param name="configManipulator">Ignored. Retained for binary compatibility.</param>
/// <param name="backOfficeSecurityAccessor">The back office security accessor.</param>
/// <param name="configManipulator">The configuration manipulator.</param>
public SetStatusRedirectUrlManagementController(
#pragma warning disable IDE0060 // Remove unused parameter
IBackOfficeSecurityAccessor backOfficeSecurityAccessor,
IConfigManipulator configManipulator)
#pragma warning restore IDE0060 // Remove unused parameter
{
_backOfficeSecurityAccessor = backOfficeSecurityAccessor;
_configManipulator = configManipulator;
}
// TODO: Consider if we should even allow this, or only allow using the appsettings
// We generally don't want to edit the appsettings from our code.
// But maybe there is a valid use case for doing it on the fly.
/// <summary>
/// Deprecated. Returns an OK response without modifying any configuration. To toggle redirect URL tracking,
/// set the <c>Umbraco:CMS:WebRouting:DisableRedirectUrlTracking</c> configuration key instead.
/// Sets the redirect URL tracking status.
/// </summary>
/// <param name="cancellationToken">The cancellation token for the HTTP request.</param>
/// <param name="status">The redirect status (ignored).</param>
/// <returns>An OK result.</returns>
/// <param name="status">The redirect status to set.</param>
/// <returns>An OK result if successful.</returns>
[HttpPost("status")]
[EndpointSummary("Deprecated. No longer changes the redirect URL tracking status.")]
[EndpointDescription("This endpoint is deprecated and no longer modifies the configuration. To toggle redirect URL tracking, set the Umbraco:CMS:WebRouting:DisableRedirectUrlTracking configuration key instead.")]
[EndpointSummary("Sets the redirect URL tracking status.")]
[EndpointDescription("Updates the redirect URL tracking configuration according to the provided status.")]
[MapToApiVersion("1.0")]
[Obsolete("This endpoint is deprecated and no longer modifies the configuration. Set the Umbraco:CMS:WebRouting:DisableRedirectUrlTracking configuration key instead. Scheduled for removal in Umbraco 19.")]
public Task<IActionResult> SetStatus(CancellationToken cancellationToken, [FromQuery] RedirectStatus status)
=> Task.FromResult<IActionResult>(Ok());
public async Task<IActionResult> SetStatus(CancellationToken cancellationToken, [FromQuery] RedirectStatus status)
{
// TODO: uncomment this when auth is implemented.
// var userIsAdmin = _backOfficeSecurityAccessor.BackOfficeSecurity?.CurrentUser?.IsAdmin();
// if (userIsAdmin is null or false)
// {
// return Unauthorized();
// }
var enable = status switch
{
RedirectStatus.Enabled => true,
RedirectStatus.Disabled => false,
_ => throw new ArgumentOutOfRangeException(nameof(status), status, "Unknown redirect status")
};
// For now I'm not gonna change this to limit breaking, but it's weird to have a "disabled" switch,
// since you're essentially negating the boolean from the get go,
// it's much easier to reason with enabled = false == disabled.
await _configManipulator.SaveDisableRedirectUrlTrackingAsync(!enable);
// Taken from the existing implementation in RedirectUrlManagementController
// TODO this is ridiculous, but we need to ensure the configuration is reloaded, before this request is ended.
// otherwise we can read the old value in GetEnableState.
// The value is equal to JsonConfigurationSource.ReloadDelay
Thread.Sleep(250);
return Ok();
}
}
@@ -28,7 +28,6 @@ public class ConfigurationServerController : ServerControllerBase
private readonly GlobalSettings _globalSettings;
private readonly IBackOfficeExternalLoginProviders _externalLoginProviders;
private readonly IHostingEnvironment _hostingEnvironment;
private readonly SignalRSettings _signalRSettings;
/// <summary>
/// Initializes a new instance of the <see cref="ConfigurationServerController"/> class.
@@ -37,38 +36,13 @@ public class ConfigurationServerController : ServerControllerBase
/// <param name="globalSettings">The global settings options.</param>
/// <param name="externalLoginProviders">The external login providers for back office.</param>
/// <param name="hostingEnvironment">The hosting environment.</param>
/// <param name="signalRSettings">The SignalR settings options.</param>
[ActivatorUtilitiesConstructor]
public ConfigurationServerController(
IOptions<SecuritySettings> securitySettings,
IOptions<GlobalSettings> globalSettings,
IBackOfficeExternalLoginProviders externalLoginProviders,
IHostingEnvironment hostingEnvironment,
IOptions<SignalRSettings> signalRSettings)
public ConfigurationServerController(IOptions<SecuritySettings> securitySettings, IOptions<GlobalSettings> globalSettings, IBackOfficeExternalLoginProviders externalLoginProviders, IHostingEnvironment hostingEnvironment)
{
_securitySettings = securitySettings.Value;
_globalSettings = globalSettings.Value;
_externalLoginProviders = externalLoginProviders;
_hostingEnvironment = hostingEnvironment;
_signalRSettings = signalRSettings.Value;
}
/// <summary>
/// Initializes a new instance of the <see cref="Umbraco.Cms.Api.Management.Controllers.Server.ConfigurationServerController"/> class.
/// </summary>
/// <param name="securitySettings">The <see cref="SecuritySettings"/> options.</param>
/// <param name="globalSettings">The <see cref="GlobalSettings"/> options.</param>
/// <param name="externalLoginProviders">The external login providers used for back office authentication.</param>
/// <param name="hostingEnvironment">The hosting environment.</param>
[Obsolete("Please use the constructor with all parameters. Scheduled for removal in Umbraco 19.")]
public ConfigurationServerController(IOptions<SecuritySettings> securitySettings, IOptions<GlobalSettings> globalSettings, IBackOfficeExternalLoginProviders externalLoginProviders, IHostingEnvironment hostingEnvironment)
: this(
securitySettings,
globalSettings,
externalLoginProviders,
hostingEnvironment,
StaticServiceProvider.Instance.GetRequiredService<IOptions<SignalRSettings>>())
{
}
/// <summary>
@@ -104,10 +78,6 @@ public class ConfigurationServerController : ServerControllerBase
VersionCheckPeriod = _globalSettings.VersionCheckPeriod,
AllowLocalLogin = _externalLoginProviders.HasDenyLocalLogin() is false,
UmbracoCssPath = _hostingEnvironment.ToAbsolute(_globalSettings.UmbracoCssPath),
SignalR = new SignalRClientSettingsResponseModel
{
SkipNegotiation = _signalRSettings.ClientShouldSkipNegotiation,
},
};
return Task.FromResult<IActionResult>(Ok(responseModel));
@@ -32,14 +32,6 @@ public class SearchTemplateItemController : TemplateItemControllerBase
_mapper = mapper;
}
/// <summary>
/// Searches for template items matching the specified query, with support for pagination.
/// </summary>
/// <param name="cancellationToken">A token to monitor for cancellation requests.</param>
/// <param name="query">The search query used to filter template items.</param>
/// <param name="skip">The number of items to skip before starting to collect the result set (used for pagination).</param>
/// <param name="take">The maximum number of items to return in the result set (used for pagination).</param>
/// <returns>A task representing the asynchronous operation. The task result contains an <see cref="IActionResult"/> with a <see cref="PagedModel{TemplateItemResponseModel}"/> containing the search results.</returns>
[HttpGet("search")]
[MapToApiVersion("1.0")]
[ProducesResponseType(typeof(PagedModel<TemplateItemResponseModel>), StatusCodes.Status200OK)]
@@ -53,14 +45,11 @@ public class SearchTemplateItemController : TemplateItemControllerBase
return Ok(new PagedModel<TemplateItemResponseModel> { Total = searchResult.Total });
}
Guid[] keys = searchResult.Items.Select(x => x.Key).ToArray();
IEnumerable<ITemplate> templates = await _templateService.GetAllAsync(keys);
IEnumerable<ITemplate> orderedTemplates = OrderByRequestedIds(templates, keys);
IEnumerable<ITemplate> templates = await _templateService.GetAllAsync(searchResult.Items.Select(item => item.Key).ToArray());
var result = new PagedModel<TemplateItemResponseModel>
{
Items = _mapper.MapEnumerable<ITemplate, TemplateItemResponseModel>(orderedTemplates),
Total = searchResult.Total,
Items = _mapper.MapEnumerable<ITemplate, TemplateItemResponseModel>(templates),
Total = searchResult.Total
};
return Ok(result);
@@ -5,9 +5,9 @@ using Umbraco.Cms.Api.Management.Services.Flags;
using Umbraco.Cms.Api.Management.ViewModels.Tree;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.Entities;
using Umbraco.Cms.Core.Services;
using Umbraco.Extensions;
namespace Umbraco.Cms.Api.Management.Controllers.Tree;
@@ -18,8 +18,11 @@ namespace Umbraco.Cms.Api.Management.Controllers.Tree;
public abstract class UserStartNodeTreeControllerBase<TItem> : EntityTreeControllerBase<TItem>
where TItem : ContentTreeItemResponseModel, new()
{
private readonly IUserStartNodeTreeFilterService _treeFilterService;
private readonly IUserStartNodeEntitiesService _userStartNodeEntitiesService;
private readonly IDataTypeService _dataTypeService;
private int[]? _userStartNodeIds;
private string[]? _userStartNodePaths;
private Dictionary<Guid, bool> _accessMap = new();
private Guid? _dataTypeKey;
@@ -36,87 +39,117 @@ public abstract class UserStartNodeTreeControllerBase<TItem> : EntityTreeControl
{
}
#pragma warning disable CS0618 // Type or member is obsolete
[Obsolete("Please use the constructor accepting IUserStartNodeTreeFilterService. Scheduled for removal in Umbraco 19.")]
protected UserStartNodeTreeControllerBase(
IEntityService entityService,
FlagProviderCollection flagProviders,
IUserStartNodeEntitiesService userStartNodeEntitiesService,
IDataTypeService dataTypeService)
: base(entityService, flagProviders)
=> _treeFilterService = new CallbackStartNodeTreeFilterService(
userStartNodeEntitiesService,
dataTypeService,
GetUserStartNodeIds,
GetUserStartNodePaths,
() => ItemObjectType);
#pragma warning restore CS0618 // Type or member is obsolete
{
_userStartNodeEntitiesService = userStartNodeEntitiesService;
_dataTypeService = dataTypeService;
}
/// <summary>
/// Initializes a new instance of the <see cref="UserStartNodeTreeControllerBase{TItem}"/> class.
/// </summary>
/// <param name="entityService">The entity service.</param>
/// <param name="flagProviders">The flag provider collection.</param>
/// <param name="treeFilterService">The user start node tree filter service.</param>
protected UserStartNodeTreeControllerBase(
IEntityService entityService,
FlagProviderCollection flagProviders,
IUserStartNodeTreeFilterService treeFilterService)
: base(entityService, flagProviders) =>
_treeFilterService = treeFilterService;
protected abstract int[] GetUserStartNodeIds();
/// <summary>
/// Gets the calculated start node IDs for the current user.
/// </summary>
/// <returns>An array of start node IDs.</returns>
[Obsolete("No longer used. Register a custom IUserStartNodeTreeFilterService instead. Scheduled for removal in Umbraco 19.")]
protected virtual int[] GetUserStartNodeIds() => [];
protected abstract string[] GetUserStartNodePaths();
/// <summary>
/// Gets the calculated start node paths for the current user.
/// </summary>
/// <returns>An array of start node paths.</returns>
[Obsolete("No longer used. Register a custom IUserStartNodeTreeFilterService instead. Scheduled for removal in Umbraco 19.")]
protected virtual string[] GetUserStartNodePaths() => [];
/// <summary>
/// Configures the controller to ignore user start nodes for a specific data type.
/// </summary>
/// <param name="dataTypeKey">The data type key, or <c>null</c> to disable.</param>
protected void IgnoreUserStartNodesForDataType(Guid? dataTypeKey) => _dataTypeKey = dataTypeKey;
/// <inheritdoc />
protected override IEntitySlim[] GetPagedRootEntities(int skip, int take, out long totalItems)
=> ShouldBypassStartNodeFiltering()
=> UserHasRootAccess() || IgnoreUserStartNodes()
? base.GetPagedRootEntities(skip, take, out totalItems)
: MapAccessEntities(_treeFilterService.GetFilteredRootEntities(out totalItems));
: CalculateAccessMap(() => _userStartNodeEntitiesService.RootUserAccessEntities(ItemObjectType, UserStartNodeIds), out totalItems);
/// <inheritdoc />
protected override IEntitySlim[] GetPagedChildEntities(Guid parentKey, int skip, int take, out long totalItems)
=> ShouldBypassStartNodeFiltering()
? base.GetPagedChildEntities(parentKey, skip, take, out totalItems)
: MapAccessEntities(_treeFilterService.GetFilteredChildEntities(parentKey, skip, take, ItemOrdering, out totalItems));
/// <inheritdoc />
protected override IEntitySlim[] GetSiblingEntities(Guid target, int before, int after, out long totalBefore, out long totalAfter)
=> ShouldBypassStartNodeFiltering()
? base.GetSiblingEntities(target, before, after, out totalBefore, out totalAfter)
: MapAccessEntities(_treeFilterService.GetFilteredSiblingEntities(target, before, after, ItemOrdering, out totalBefore, out totalAfter));
/// <inheritdoc />
protected override TItem[] MapTreeItemViewModels(Guid? parentKey, IEntitySlim[] entities)
=> ShouldBypassStartNodeFiltering()
? base.MapTreeItemViewModels(parentKey, entities)
: _treeFilterService.MapWithAccessFiltering(
entities,
_accessMap,
entity => MapTreeItemViewModel(parentKey, entity),
entity => MapTreeItemViewModelAsNoAccess(parentKey, entity));
private IEntitySlim[] MapAccessEntities(UserAccessEntity[] userAccessEntities)
{
if (UserHasRootAccess() || IgnoreUserStartNodes())
{
return base.GetPagedChildEntities(parentKey, skip, take, out totalItems);
}
IEnumerable<UserAccessEntity> userAccessEntities = _userStartNodeEntitiesService.ChildUserAccessEntities(
ItemObjectType,
UserStartNodePaths,
parentKey,
skip,
take,
ItemOrdering,
out totalItems);
return CalculateAccessMap(() => userAccessEntities, out _);
}
protected override IEntitySlim[] GetSiblingEntities(Guid target, int before, int after, out long totalBefore, out long totalAfter)
{
if (UserHasRootAccess() || IgnoreUserStartNodes())
{
return base.GetSiblingEntities(target, before, after, out totalBefore, out totalAfter);
}
IEnumerable<UserAccessEntity> userAccessEntities = _userStartNodeEntitiesService.SiblingUserAccessEntities(
ItemObjectType,
UserStartNodePaths,
target,
before,
after,
ItemOrdering,
out totalBefore,
out totalAfter);
return CalculateAccessMap(() => userAccessEntities, out _);
}
protected override TItem[] MapTreeItemViewModels(Guid? parentKey, IEntitySlim[] entities)
{
if (UserHasRootAccess() || IgnoreUserStartNodes())
{
return base.MapTreeItemViewModels(parentKey, entities);
}
// for users with no root access, only add items for the entities contained within the calculated access map.
// the access map may contain entities that the user does not have direct access to, but need still to see,
// because it has descendants that the user *does* have access to. these entities are added as "no access" items.
TItem[] contentTreeItemViewModels = entities.Select(entity =>
{
if (_accessMap.TryGetValue(entity.Key, out var hasAccess) == false)
{
// entity is not a part of the calculated access map
return null;
}
// direct access => return a regular item
// no direct access => return a "no access" item
return hasAccess
? MapTreeItemViewModel(parentKey, entity)
: MapTreeItemViewModelAsNoAccess(parentKey, entity);
})
.WhereNotNull()
.ToArray();
return contentTreeItemViewModels;
}
private int[] UserStartNodeIds => _userStartNodeIds ??= GetUserStartNodeIds();
private string[] UserStartNodePaths => _userStartNodePaths ??= GetUserStartNodePaths();
private bool UserHasRootAccess() => UserStartNodeIds.Contains(Constants.System.Root);
private bool IgnoreUserStartNodes()
=> _dataTypeKey.HasValue
&& _dataTypeService.IsDataTypeIgnoringUserStartNodes(_dataTypeKey.Value);
private IEntitySlim[] CalculateAccessMap(Func<IEnumerable<UserAccessEntity>> getUserAccessEntities, out long totalItems)
{
UserAccessEntity[] userAccessEntities = getUserAccessEntities().ToArray();
_accessMap = userAccessEntities.ToDictionary(uae => uae.Entity.Key, uae => uae.HasAccess);
return userAccessEntities.Select(uae => uae.Entity).ToArray();
IEntitySlim[] entities = userAccessEntities.Select(uae => uae.Entity).ToArray();
totalItems = entities.Length;
return entities;
}
private TItem MapTreeItemViewModelAsNoAccess(Guid? parentKey, IEntitySlim entity)
@@ -125,45 +158,4 @@ public abstract class UserStartNodeTreeControllerBase<TItem> : EntityTreeControl
viewModel.NoAccess = true;
return viewModel;
}
private bool ShouldBypassStartNodeFiltering()
=> _treeFilterService.ShouldBypassStartNodeFiltering(_dataTypeKey);
/// <summary>
/// A backward-compatible adapter that implements <see cref="UserStartNodeTreeFilterService"/>
/// by delegating start node resolution to callback functions.
/// </summary>
/// <remarks>
/// Used by the obsolete constructor to bridge the old abstract-method-based
/// start node resolution to the new service-based approach.
/// </remarks>
[Obsolete("Only used by the obsolete constructor. Scheduled for removal in Umbraco 19.")]
private sealed class CallbackStartNodeTreeFilterService : UserStartNodeTreeFilterService
{
private readonly Func<int[]> _getStartNodeIds;
private readonly Func<string[]> _getStartNodePaths;
private readonly Func<UmbracoObjectTypes> _getTreeObjectType;
public CallbackStartNodeTreeFilterService(
IUserStartNodeEntitiesService userStartNodeEntitiesService,
IDataTypeService dataTypeService,
Func<int[]> getStartNodeIds,
Func<string[]> getStartNodePaths,
Func<UmbracoObjectTypes> getTreeObjectType)
: base(userStartNodeEntitiesService, dataTypeService)
{
_getStartNodeIds = getStartNodeIds;
_getStartNodePaths = getStartNodePaths;
_getTreeObjectType = getTreeObjectType;
}
/// <inheritdoc />
protected override UmbracoObjectTypes TreeObjectType => _getTreeObjectType();
/// <inheritdoc />
protected override int[] CalculateUserStartNodeIds() => _getStartNodeIds();
/// <inheritdoc />
protected override string[] CalculateUserStartNodePaths() => _getStartNodePaths();
}
}
@@ -1,54 +0,0 @@
using Asp.Versioning;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Core.Security;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Services.OperationStatus;
namespace Umbraco.Cms.Api.Management.Controllers.User.Current;
/// <summary>
/// Controller responsible for handling requests to clear the avatar of the currently authenticated user.
/// </summary>
[ApiVersion("1.0")]
public class ClearAvatarCurrentUserController : CurrentUserControllerBase
{
private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor;
private readonly IUserService _userService;
/// <summary>
/// Initializes a new instance of the <see cref="ClearAvatarCurrentUserController"/> class.
/// </summary>
/// <param name="backOfficeSecurityAccessor">Provides access to back office security features for the current user.</param>
/// <param name="userService">Service for managing user-related operations.</param>
public ClearAvatarCurrentUserController(
IBackOfficeSecurityAccessor backOfficeSecurityAccessor,
IUserService userService)
{
_backOfficeSecurityAccessor = backOfficeSecurityAccessor;
_userService = userService;
}
/// <summary>
/// Removes the avatar image for the currently authenticated user.
/// </summary>
/// <param name="cancellationToken">A cancellation token to cancel the operation.</param>
/// <returns>An <see cref="IActionResult"/> indicating the result of the operation.</returns>
[MapToApiVersion("1.0")]
[HttpDelete("avatar")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
[EndpointSummary("Clears the current user's avatar.")]
[EndpointDescription("Removes the avatar image for the currently authenticated user.")]
public async Task<IActionResult> ClearAvatar(CancellationToken cancellationToken)
{
Guid userKey = CurrentUserKey(_backOfficeSecurityAccessor);
UserOperationStatus result = await _userService.ClearAvatarAsync(userKey);
return result is UserOperationStatus.Success
? Ok()
: UserOperationStatusResult(result);
}
}
@@ -59,7 +59,7 @@ public class GetCurrentUserController : CurrentUserControllerBase
[EndpointDescription("Gets the currently authenticated back office user's information and permissions.")]
public async Task<IActionResult> GetCurrentUser(CancellationToken cancellationToken)
{
Guid currentUserKey = CurrentUserKey(_backOfficeSecurityAccessor);
var currentUserKey = CurrentUserKey(_backOfficeSecurityAccessor);
AuthorizationResult authorizationResult = await _authorizationService.AuthorizeResourceAsync(
User,
@@ -78,7 +78,7 @@ public class GetCurrentUserController : CurrentUserControllerBase
return Unauthorized();
}
CurrentUserResponseModel responseModel = await _userPresentationFactory.CreateCurrentUserResponseModelAsync(user);
var responseModel = await _userPresentationFactory.CreateCurrentUserResponseModelAsync(user);
return Ok(responseModel);
}
}
@@ -1,12 +1,10 @@
using Asp.Versioning;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.DependencyInjection;
using Umbraco.Cms.Api.Management.ViewModels.User.Current;
using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Mapping;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.Membership;
using Umbraco.Cms.Core.Security;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Services.OperationStatus;
@@ -20,46 +18,23 @@ namespace Umbraco.Cms.Api.Management.Controllers.User.Current;
public class GetDocumentPermissionsCurrentUserController : CurrentUserControllerBase
{
private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor;
private readonly IUserService _userService;
private readonly IUmbracoMapper _mapper;
private readonly IContentPermissionService _contentPermissionService;
// TODO (V19): Remove the IUserService parameter from the constructor as it is not used in the current implementation.
/// <summary>
/// Initializes a new instance of the <see cref="GetDocumentPermissionsCurrentUserController"/> class, which handles requests related to retrieving document permissions for the current user.
/// </summary>
/// <param name="backOfficeSecurityAccessor">Provides access to back office security information for the current user.</param>
/// <param name="mapper">The Umbraco object mapper used for mapping between models.</param>
/// <param name="contentPermissionService">Service for managing content permissions.</param>
[ActivatorUtilitiesConstructor]
public GetDocumentPermissionsCurrentUserController(
IBackOfficeSecurityAccessor backOfficeSecurityAccessor,
IUserService userService,
IUmbracoMapper mapper,
IContentPermissionService contentPermissionService)
{
_backOfficeSecurityAccessor = backOfficeSecurityAccessor;
_mapper = mapper;
_contentPermissionService = contentPermissionService;
}
/// <summary>
/// Initializes a new instance of the <see cref="GetDocumentPermissionsCurrentUserController"/> class.
/// </summary>
/// <param name="backOfficeSecurityAccessor">Provides access to back office security information for the current user.</param>
/// <param name="userService">Service for managing and retrieving user information.</param>
/// <param name="mapper">The Umbraco object mapper used for mapping between models.</param>
[Obsolete("Please use the constructor with all parameters. Scheduled for removal in Umbraco 19.")]
public GetDocumentPermissionsCurrentUserController(
IBackOfficeSecurityAccessor backOfficeSecurityAccessor,
IUserService userService,
IUmbracoMapper mapper)
: this(
backOfficeSecurityAccessor,
userService,
mapper,
StaticServiceProvider.Instance.GetRequiredService<IContentPermissionService>())
{
_backOfficeSecurityAccessor = backOfficeSecurityAccessor;
_userService = userService;
_mapper = mapper;
}
/// <summary>
@@ -67,10 +42,10 @@ public class GetDocumentPermissionsCurrentUserController : CurrentUserController
/// </summary>
/// <param name="cancellationToken">A token to monitor for cancellation requests.</param>
/// <param name="ids">A set of document IDs for which to retrieve permissions.</param>
/// <returns>An <see cref="IActionResult"/> containing a <see cref="UserPermissionsResponseModel"/> with the permissions for each requested document.</returns>
/// <returns>An <see cref="IActionResult"/> containing a <see cref="UserPermissionsResponseModel"/> with the permissions for each requested document, or a <see cref="ProblemDetails"/> if not found.</returns>
[MapToApiVersion("1.0")]
[HttpGet("permissions/document")]
[ProducesResponseType(typeof(UserPermissionsResponseModel), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(IEnumerable<UserPermissionsResponseModel>), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
[EndpointSummary("Gets document permissions for the current user.")]
[EndpointDescription("Gets the document permissions for the currently authenticated user.")]
@@ -78,16 +53,14 @@ public class GetDocumentPermissionsCurrentUserController : CurrentUserController
CancellationToken cancellationToken,
[FromQuery(Name = "id")] HashSet<Guid> ids)
{
IUser currentUser = CurrentUser(_backOfficeSecurityAccessor);
NodePermissions[] permissions = (await _contentPermissionService.GetPermissionsAsync(currentUser, ids)).ToArray();
Attempt<IEnumerable<NodePermissions>, UserOperationStatus> permissionsAttempt = await _userService.GetDocumentPermissionsAsync(CurrentUserKey(_backOfficeSecurityAccessor), ids);
// Preserve 404 behavior: if any requested ID was not found, return ContentNodeNotFound.
if (ids.Count > 0 && permissions.Length < ids.Count)
if (permissionsAttempt.Success is false)
{
return UserOperationStatusResult(UserOperationStatus.ContentNodeNotFound);
return UserOperationStatusResult(permissionsAttempt.Status);
}
List<UserPermissionViewModel> viewModels = _mapper.MapEnumerable<NodePermissions, UserPermissionViewModel>(permissions);
List<UserPermissionViewModel> viewModels = _mapper.MapEnumerable<NodePermissions, UserPermissionViewModel>(permissionsAttempt.Result);
return Ok(new UserPermissionsResponseModel { Permissions = viewModels });
}
@@ -20,6 +20,7 @@ namespace Umbraco.Cms.Api.Management.Controllers.User.Current;
public class SetAvatarCurrentUserController : CurrentUserControllerBase
{
private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor;
private readonly IAuthorizationService _authorizationService;
private readonly IUserService _userService;
/// <summary>
@@ -28,15 +29,13 @@ public class SetAvatarCurrentUserController : CurrentUserControllerBase
/// <param name="backOfficeSecurityAccessor">Provides access to back office security features for the current user.</param>
/// <param name="authorizationService">Service used to authorize user actions.</param>
/// <param name="userService">Service for managing user-related operations.</param>
// TODO (V18): Remove the IAuthorizationService parameter from the constructor and the class, as it is not used in the current implementation.
public SetAvatarCurrentUserController(
IBackOfficeSecurityAccessor backOfficeSecurityAccessor,
#pragma warning disable IDE0060 // Remove unused parameter
IAuthorizationService authorizationService,
#pragma warning restore IDE0060 // Remove unused parameter
IUserService userService)
{
_backOfficeSecurityAccessor = backOfficeSecurityAccessor;
_authorizationService = authorizationService;
_userService = userService;
}
@@ -56,6 +55,16 @@ public class SetAvatarCurrentUserController : CurrentUserControllerBase
{
Guid userKey = CurrentUserKey(_backOfficeSecurityAccessor);
AuthorizationResult authorizationResult = await _authorizationService.AuthorizeResourceAsync(
User,
UserPermissionResource.WithKeys(userKey),
AuthorizationPolicies.UserPermissionByResource);
if (!authorizationResult.Succeeded)
{
return Forbidden();
}
UserOperationStatus result = await _userService.SetAvatarAsync(userKey, model.File.Id);
return result is UserOperationStatus.Success
@@ -1,64 +0,0 @@
using Asp.Versioning;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Api.Management.Factories;
using Umbraco.Cms.Api.Management.ViewModels.User;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.Membership;
using Umbraco.Cms.Core.Security;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Services.OperationStatus;
namespace Umbraco.Cms.Api.Management.Controllers.User.Current;
/// <summary>
/// Controller responsible for update information about the currently authenticated user.
/// </summary>
[ApiVersion("1.0")]
public class UpdateCurrentUserProfileController : CurrentUserControllerBase
{
private readonly IUserService _userService;
private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor;
private readonly IUserPresentationFactory _userPresentationFactory;
/// <summary>
/// Initializes a new instance of the <see cref="UpdateCurrentUserProfileController"/> class, which manages user update operations in the Umbraco backoffice API.
/// </summary>
/// <param name="userService">Service for managing user data and operations.</param>
/// <param name="userPresentationFactory">Factory for creating user presentation models.</param>
/// <param name="backOfficeSecurityAccessor">Accessor for back office security context.</param>
public UpdateCurrentUserProfileController(
IUserService userService,
IUserPresentationFactory userPresentationFactory,
IBackOfficeSecurityAccessor backOfficeSecurityAccessor)
{
_userService = userService;
_backOfficeSecurityAccessor = backOfficeSecurityAccessor;
_userPresentationFactory = userPresentationFactory;
}
/// <summary>
/// Updates the current user with new details provided in the request model.
/// </summary>
/// <param name="model">The request model containing updated current user information.</param>
/// <returns>An <see cref="IActionResult"/> indicating the outcome of the update operation.</returns>
[HttpPut("profile")]
[MapToApiVersion("1.0")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
[EndpointSummary("Updates current user profile.")]
[EndpointDescription("Updates current user profile with the details from the request model.")]
public async Task<IActionResult> UpdateCurrentUser(UpdateCurrentUserRequestModel model)
{
Guid userKey = CurrentUserKey(_backOfficeSecurityAccessor);
UserUpdateProfileModel updateModel = await _userPresentationFactory.CreateUpdateProfileModelAsync(model);
Attempt<IUser?, UserOperationStatus> result = await _userService.UpdateProfileAsync(userKey, updateModel);
return result.Success
? Ok()
: UserOperationStatusResult(result.Status);
}
}
@@ -63,10 +63,6 @@ public abstract class UserOrCurrentUserControllerBase : ManagementApiControllerB
.WithTitle("Cannot delete user")
.WithDetail("The user cannot be deleted.")
.Build()),
UserOperationStatus.CannotDeleteUserWithLoginHistory => BadRequest(problemDetailsBuilder
.WithTitle("Cannot delete user")
.WithDetail("This user has logged in and may be referenced by audit logs or content history. Disable the user instead of deleting them.")
.Build()),
UserOperationStatus.CannotDisableSelf => BadRequest(problemDetailsBuilder
.WithTitle("Cannot disable")
.WithDetail("A user cannot disable itself.")
@@ -1,7 +1,6 @@
using Microsoft.Extensions.DependencyInjection;
using Umbraco.Cms.Api.Management.Factories;
using Umbraco.Cms.Api.Management.Mapping.Member;
using Umbraco.Cms.Api.Management.Services;
using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Core.Mapping;
@@ -13,8 +12,6 @@ internal static class MemberBuilderExtensions
{
builder.Services.AddSingleton<IMemberPresentationFactory, MemberPresentationFactory>();
builder.Services.AddTransient<IMemberEditingPresentationFactory, MemberEditingPresentationFactory>();
builder.Services.AddTransient<IMemberPresentationService, MemberPresentationService>();
builder.Services.AddTransient<IMemberReferenceService, MemberReferenceService>();
builder.WithCollectionBuilder<MapDefinitionCollectionBuilder>().Add<MemberMapDefinition>();
@@ -11,8 +11,6 @@ internal static class TreeBuilderExtensions
internal static IUmbracoBuilder AddTrees(this IUmbracoBuilder builder)
{
builder.Services.AddTransient<IUserStartNodeEntitiesService, UserStartNodeEntitiesService>();
builder.Services.AddTransient<IDocumentStartNodeTreeFilterService, DocumentStartNodeTreeFilterService>();
builder.Services.AddTransient<IMediaStartNodeTreeFilterService, MediaStartNodeTreeFilterService>();
builder.Services.AddUnique<IPartialViewTreeService, PartialViewTreeService>();
builder.Services.AddUnique<IScriptTreeService, ScriptTreeService>();
@@ -5,7 +5,6 @@ using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Core.Hosting;
using Umbraco.Cms.Core.IO;
using Umbraco.Cms.Web.Common.Hosting;
using Umbraco.Cms.Web.Common.Middleware;
namespace Umbraco.Extensions;
@@ -69,10 +68,6 @@ public static partial class UmbracoBuilderExtensions
builder.Services.AddSingleton<IBackOfficeEnabledMarker, BackOfficeEnabledMarker>();
builder.Services.AddUnique<IBackOfficePathGenerator, UmbracoBackOfficePathGenerator>();
// Registered here rather than in AddWebComponents because the middleware depends on
// IBackOfficePathGenerator (registered just above). DI scope validation would otherwise
// fail in Delivery-only/Website-only bootstraps that never call AddBackOffice().
builder.Services.AddSingleton<UmbracoBackOfficeCacheHeadersMiddleware>();
builder.Services.AddUnique<IPhysicalFileSystem>(factory =>
{
var path = "~/";
@@ -54,8 +54,7 @@ public static partial class UmbracoBuilderExtensions
factory.GetRequiredService<IUserRepository>(),
factory.GetRequiredService<IRuntimeState>(),
factory.GetRequiredService<IEventMessagesFactory>(),
factory.GetRequiredService<ILogger<BackOfficeUserStore>>(),
factory.GetRequiredService<IBackOfficeUserReader>()))
factory.GetRequiredService<ILogger<BackOfficeUserStore>>()))
.AddUserManager<IBackOfficeUserManager, BackOfficeUserManager>()
.AddSignInManager<IBackOfficeSignInManager, BackOfficeSignInManager>()
.AddClaimsPrincipalFactory<BackOfficeClaimsPrincipalFactory>()
@@ -1,8 +1,5 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Umbraco.Cms.Api.Management.ViewModels.DataType;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.PropertyEditors;
using Umbraco.Cms.Core.Serialization;
@@ -19,7 +16,6 @@ public class DataTypePresentationFactory : IDataTypePresentationFactory
private readonly IDataValueEditorFactory _dataValueEditorFactory;
private readonly IConfigurationEditorJsonSerializer _configurationEditorJsonSerializer;
private readonly TimeProvider _timeProvider;
private readonly ILogger<DataTypePresentationFactory> _logger;
/// <summary>
/// Initializes a new instance of the <see cref="DataTypePresentationFactory"/> class, which is responsible for creating data type presentation models.
@@ -29,46 +25,18 @@ public class DataTypePresentationFactory : IDataTypePresentationFactory
/// <param name="dataValueEditorFactory">Factory for creating data value editors.</param>
/// <param name="configurationEditorJsonSerializer">Serializer for configuration editor JSON data.</param>
/// <param name="timeProvider">Provides the current time for time-dependent operations.</param>
/// <param name="logger">The logger.</param>
public DataTypePresentationFactory(
IDataTypeContainerService dataTypeContainerService,
PropertyEditorCollection propertyEditorCollection,
IDataValueEditorFactory dataValueEditorFactory,
IConfigurationEditorJsonSerializer configurationEditorJsonSerializer,
TimeProvider timeProvider,
ILogger<DataTypePresentationFactory> logger)
{
_dataTypeContainerService = dataTypeContainerService;
_propertyEditorCollection = propertyEditorCollection;
_dataValueEditorFactory = dataValueEditorFactory;
_configurationEditorJsonSerializer = configurationEditorJsonSerializer;
_timeProvider = timeProvider;
_logger = logger;
}
/// <summary>
/// Initializes a new instance of the <see cref="DataTypePresentationFactory"/> class, which is responsible for creating data type presentation models.
/// </summary>
/// <param name="dataTypeContainerService">Service used to manage data type containers.</param>
/// <param name="propertyEditorCollection">A collection containing all available property editors.</param>
/// <param name="dataValueEditorFactory">Factory for creating data value editors.</param>
/// <param name="configurationEditorJsonSerializer">Serializer for configuration editor JSON data.</param>
/// <param name="timeProvider">Provides the current time for time-dependent operations.</param>
[Obsolete("Please use the constructor that takes all parameters. Scheduled for removal in Umbraco 19.")]
public DataTypePresentationFactory(
IDataTypeContainerService dataTypeContainerService,
PropertyEditorCollection propertyEditorCollection,
IDataValueEditorFactory dataValueEditorFactory,
IConfigurationEditorJsonSerializer configurationEditorJsonSerializer,
TimeProvider timeProvider)
: this(
dataTypeContainerService,
propertyEditorCollection,
dataValueEditorFactory,
configurationEditorJsonSerializer,
timeProvider,
StaticServiceProvider.Instance.GetRequiredService<ILogger<DataTypePresentationFactory>>())
{
_dataTypeContainerService = dataTypeContainerService;
_propertyEditorCollection = propertyEditorCollection;
_dataValueEditorFactory = dataValueEditorFactory;
_configurationEditorJsonSerializer = configurationEditorJsonSerializer;
_timeProvider = timeProvider;
}
/// <inheritdoc />
@@ -104,6 +72,7 @@ public class DataTypePresentationFactory : IDataTypePresentationFactory
dataType.Key = requestModel.Id.Value;
}
return Attempt.SucceedWithStatus<IDataType, DataTypeOperationStatus>(DataTypeOperationStatus.Success, dataType);
}
@@ -113,7 +82,7 @@ public class DataTypePresentationFactory : IDataTypePresentationFactory
{
try
{
EntityContainer? parent = await _dataTypeContainerService.GetAsync(requestModel.Parent.Id);
var parent = await _dataTypeContainerService.GetAsync(requestModel.Parent.Id);
return parent is null
? Attempt.FailWithStatus(DataTypeOperationStatus.ParentNotFound, 0)
@@ -128,7 +97,6 @@ public class DataTypePresentationFactory : IDataTypePresentationFactory
return Attempt.SucceedWithStatus(DataTypeOperationStatus.Success, Constants.System.Root);
}
/// <inheritdoc/>
public Task<Attempt<IDataType, DataTypeOperationStatus>> CreateAsync(UpdateDataTypeRequestModel requestModel, IDataType current)
{
if (!_propertyEditorCollection.TryGet(requestModel.EditorAlias, out IDataEditor? editor))
@@ -136,7 +104,7 @@ public class DataTypePresentationFactory : IDataTypePresentationFactory
return Task.FromResult(Attempt.FailWithStatus<IDataType, DataTypeOperationStatus>(DataTypeOperationStatus.PropertyEditorNotFound, new DataType(new VoidEditor(_dataValueEditorFactory), _configurationEditorJsonSerializer) ));
}
var dataType = (IDataType)current.DeepClone();
IDataType dataType = (IDataType)current.DeepClone();
IDictionary<string, object> configurationData = MapConfigurationData(requestModel, editor);
dataType.Name = requestModel.Name;
@@ -151,26 +119,12 @@ public class DataTypePresentationFactory : IDataTypePresentationFactory
private ValueStorageType GetEditorValueStorageType(IDataEditor editor, IDictionary<string, object> configurationData)
{
// Only editors whose configuration object implements IConfigureValueType derive their storage
// type from the configuration. Building the typed configuration object can throw for editors
// whose stored configuration doesn't cleanly deserialize into their configuration type; that
// must not fail the save, so fall back to the value editor's value type in that case.
try
var configurationObject = editor.GetConfigurationEditor()
.ToConfigurationObject(configurationData, _configurationEditorJsonSerializer);
if (configurationObject is IConfigureValueType configureValueType)
{
if (editor.GetConfigurationEditor().ToConfigurationObject(configurationData, _configurationEditorJsonSerializer)
is IConfigureValueType configureValueType)
{
return ValueTypes.ToStorageType(configureValueType.ValueType);
}
}
catch (Exception)
{
// Configuration editors are third-party and can throw anything when the stored configuration
// doesn't deserialize into their configuration type. Fall back to the value editor's value type
// rather than failing the save, but log so the misconfiguration remains observable.
_logger.LogError(
"Could not build the configuration object for editor {EditorAlias} to determine its value storage type; falling back to the value editor's value type.",
editor.Alias);
return ValueTypes.ToStorageType(configureValueType.ValueType);
}
var valueType = editor.GetValueEditor().ValueType;
@@ -102,7 +102,7 @@ public class DocumentUrlFactory : IDocumentUrlFactory
if (await _previewService.TryEnterPreviewAsync(currentUser) is false)
{
_logger.LogError("A server error occurred, could not initiate an authenticated preview state for the current user.");
_logger.LogError("A server error occured, could not initiate an authenticated preview state for the current user.");
return null;
}
}
@@ -3,7 +3,6 @@ using Umbraco.Cms.Api.Management.ViewModels.Member.Item;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.Entities;
using Umbraco.Cms.Core.Models.Membership;
using Umbraco.Cms.Core.Security;
namespace Umbraco.Cms.Api.Management.Factories;
@@ -41,31 +40,4 @@ public interface IMemberPresentationFactory
/// <param name="entity">The member entity to create the response model from.</param>
/// <returns>A MemberItemResponseModel representing the member entity.</returns>
MemberItemResponseModel CreateItemResponseModel(IMember entity);
/// <summary>
/// Creates a response model for an external-only member.
/// </summary>
/// <param name="member">The external member identity to create the response model from.</param>
/// <returns>A task that represents the asynchronous operation. The task result contains the <see cref="MemberResponseModel"/>.</returns>
// TODO (V19): Remove the default implementation.
Task<MemberResponseModel> CreateExternalMemberResponseModelAsync(ExternalMemberIdentity member)
=> Task.FromResult(new MemberResponseModel { Id = member.Key, Kind = MemberKind.ExternalOnly });
/// <summary>
/// Creates an item response model for an external-only member.
/// </summary>
/// <param name="member">The external member identity to create the item response model from.</param>
/// <returns>A <see cref="MemberItemResponseModel"/> representing the external member.</returns>
// TODO (V19): Remove the default implementation.
MemberItemResponseModel CreateExternalMemberItemResponseModel(ExternalMemberIdentity member)
=> new() { Id = member.Key, Kind = MemberKind.ExternalOnly };
/// <summary>
/// Creates a response model from a <see cref="MemberFilterItem"/> returned by the combined filter query.
/// </summary>
/// <param name="item">The filter item to create the response model from.</param>
/// <returns>A <see cref="MemberResponseModel"/> representing the filter item.</returns>
// TODO (V19): Remove the default implementation.
MemberResponseModel CreateFilterItemResponseModel(MemberFilterItem item)
=> new() { Id = item.Key, Kind = item.Kind };
}
@@ -31,12 +31,6 @@ public interface IUserPresentationFactory
/// </summary>
Task<UserUpdateModel> CreateUpdateModelAsync(Guid existingUserKey, UpdateUserRequestModel updateModel);
/// <summary>
/// Creates an update model for a current user based on the provided request model.
/// </summary>
// TODO V19: Remove default implementation
Task<UserUpdateProfileModel> CreateUpdateProfileModelAsync(UpdateCurrentUserRequestModel updateModel) => throw new NotImplementedException();
/// <summary>
/// Creates a response model for the current user based on the provided user.
/// </summary>
@@ -62,10 +56,10 @@ public interface IUserPresentationFactory
/// </summary>
UserItemResponseModel CreateItemResponseModel(IUser user);
/// <summary>
/// Asynchronously creates a response model containing the calculated start nodes for the specified user.
/// </summary>
/// <param name="user">The user for whom to calculate start nodes.</param>
/// <returns>A task representing the asynchronous operation. The task result contains the calculated user start nodes response model.</returns>
/// <summary>
/// Asynchronously creates a response model containing the calculated start nodes for the specified user.
/// </summary>
/// <param name="user">The user for whom to calculate start nodes.</param>
/// <returns>A task representing the asynchronous operation. The task result contains the calculated user start nodes response model.</returns>
Task<CalculatedUserStartNodesResponseModel> CreateCalculatedUserStartNodesResponseModelAsync(IUser user);
}
@@ -124,7 +124,7 @@ public class IndexPresentationFactory : IIndexPresentationFactory
}
catch (Exception e)
{
_logger.LogError(e, "An error occurred trying to get the searcher name of index {IndexName}", index.Name);
_logger.LogError(e, "An error occured trying to get the searcher name of index {IndexName}", index.Name);
name = "Could not determine searcher name because of error.";
return false;
}
@@ -139,7 +139,7 @@ public class IndexPresentationFactory : IIndexPresentationFactory
}
catch (Exception e)
{
_logger.LogError(e, "An error occurred trying to get the document count of index {IndexName}", index.Name);
_logger.LogError(e, "An error occured trying to get the document count of index {IndexName}", index.Name);
documentCount = 0;
return false;
}
@@ -154,7 +154,7 @@ public class IndexPresentationFactory : IIndexPresentationFactory
}
catch (Exception e)
{
_logger.LogError(e, "An error occurred trying to get the field name count of index {IndexName}", index.Name);
_logger.LogError(e, "An error occured trying to get the field name count of index {IndexName}", index.Name);
fieldNameCount = 0;
return false;
}
@@ -9,13 +9,11 @@ using Umbraco.Cms.Core.Mapping;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.Entities;
using Umbraco.Cms.Core.Models.Membership;
using Umbraco.Cms.Core.Security;
using Umbraco.Cms.Core.Services;
using Umbraco.Extensions;
namespace Umbraco.Cms.Api.Management.Factories;
/// <inheritdoc/>
internal sealed class MemberPresentationFactory : IMemberPresentationFactory
{
private readonly IUmbracoMapper _umbracoMapper;
@@ -24,7 +22,6 @@ internal sealed class MemberPresentationFactory : IMemberPresentationFactory
private readonly ITwoFactorLoginService _twoFactorLoginService;
private readonly IMemberGroupService _memberGroupService;
private readonly DeliveryApiSettings _deliveryApiSettings;
private readonly IExternalMemberService _externalMemberService;
private IEnumerable<Guid>? _clientCredentialsMemberKeys;
/// <summary>
@@ -36,15 +33,13 @@ internal sealed class MemberPresentationFactory : IMemberPresentationFactory
/// <param name="twoFactorLoginService">Service for handling two-factor authentication for members.</param>
/// <param name="memberGroupService">Service for managing member groups.</param>
/// <param name="deliveryApiSettings">The configuration options for the Delivery API.</param>
/// <param name="externalMemberService">Service for managing external-only members.</param>
public MemberPresentationFactory(
IUmbracoMapper umbracoMapper,
IMemberService memberService,
IMemberTypeService memberTypeService,
ITwoFactorLoginService twoFactorLoginService,
IMemberGroupService memberGroupService,
IOptions<DeliveryApiSettings> deliveryApiSettings,
IExternalMemberService externalMemberService)
IOptions<DeliveryApiSettings> deliveryApiSettings)
{
_umbracoMapper = umbracoMapper;
_memberService = memberService;
@@ -52,10 +47,14 @@ internal sealed class MemberPresentationFactory : IMemberPresentationFactory
_twoFactorLoginService = twoFactorLoginService;
_memberGroupService = memberGroupService;
_deliveryApiSettings = deliveryApiSettings.Value;
_externalMemberService = externalMemberService;
}
/// <inheritdoc/>
/// <summary>
/// Asynchronously creates a <see cref="MemberResponseModel"/> for the specified <see cref="IMember"/>, including or excluding sensitive data based on the current user's permissions.
/// </summary>
/// <param name="member">The member entity to map to a response model.</param>
/// <param name="currentUser">The user requesting the data, used to determine access to sensitive information.</param>
/// <returns>A task representing the asynchronous operation, with a <see cref="MemberResponseModel"/> as the result.</returns>
public async Task<MemberResponseModel> CreateResponseModelAsync(IMember member, IUser currentUser)
{
MemberResponseModel responseModel = _umbracoMapper.Map<MemberResponseModel>(member)!;
@@ -71,7 +70,6 @@ internal sealed class MemberPresentationFactory : IMemberPresentationFactory
: await RemoveSensitiveDataAsync(member, responseModel);
}
/// <inheritdoc/>
public async Task<IEnumerable<MemberResponseModel>> CreateMultipleAsync(IEnumerable<IMember> members, IUser currentUser)
{
var memberResponseModels = new List<MemberResponseModel>();
@@ -83,101 +81,41 @@ internal sealed class MemberPresentationFactory : IMemberPresentationFactory
return memberResponseModels;
}
/// <inheritdoc/>
/// <summary>
/// Creates a response model for a member item from the given entity.
/// </summary>
/// <param name="entity">The member entity to create the response model from.</param>
/// <returns>A <see cref="MemberItemResponseModel"/> representing the member.</returns>
public MemberItemResponseModel CreateItemResponseModel(IMemberEntitySlim entity)
=> CreateItemResponseModel<IMemberEntitySlim>(entity);
/// <inheritdoc/>
/// <summary>
/// Creates a response model for a member item based on the given member entity.
/// </summary>
/// <param name="entity">The member entity to create the response model from.</param>
/// <returns>A <see cref="MemberItemResponseModel"/> representing the member.</returns>
public MemberItemResponseModel CreateItemResponseModel(IMember entity)
=> CreateItemResponseModel<IMember>(entity);
/// <inheritdoc/>
public async Task<MemberResponseModel> CreateExternalMemberResponseModelAsync(ExternalMemberIdentity member)
{
IEnumerable<string> roles = await _externalMemberService.GetRolesAsync(member.Key);
IEnumerable<Guid> groupKeys = roles
.Select(x => _memberGroupService.GetByName(x))
.WhereNotNull()
.Select(x => x.Key)
.ToArray();
return new MemberResponseModel
{
Id = member.Key,
Email = member.Email,
Username = member.UserName,
IsApproved = member.IsApproved,
IsLockedOut = member.IsLockedOut,
IsTwoFactorEnabled = false,
FailedPasswordAttempts = 0,
LastLoginDate = member.LastLoginDate.HasValue ? new DateTimeOffset(member.LastLoginDate.Value, TimeSpan.Zero) : null,
LastLockoutDate = member.LastLockoutDate.HasValue ? new DateTimeOffset(member.LastLockoutDate.Value, TimeSpan.Zero) : null,
LastPasswordChangeDate = null,
Kind = MemberKind.ExternalOnly,
Variants = [new MemberVariantResponseModel
{
Name = member.Name ?? string.Empty,
CreateDate = new DateTimeOffset(member.CreateDate, TimeSpan.Zero),
UpdateDate = new DateTimeOffset(member.UpdateDate, TimeSpan.Zero),
}],
Values = Enumerable.Empty<MemberValueResponseModel>(),
MemberType = new MemberTypeReferenceResponseModel(),
Groups = groupKeys,
ProfileData = member.ProfileData,
};
}
/// <inheritdoc/>
public MemberItemResponseModel CreateExternalMemberItemResponseModel(ExternalMemberIdentity member) =>
new()
{
Id = member.Key,
MemberType = new MemberTypeReferenceResponseModel(),
Variants = [new VariantItemResponseModel { Name = member.Name ?? string.Empty, Culture = null }],
Kind = MemberKind.ExternalOnly,
};
/// <inheritdoc/>
public MemberResponseModel CreateFilterItemResponseModel(MemberFilterItem item) =>
new()
{
Id = item.Key,
Email = item.Email,
Username = item.UserName,
IsApproved = item.IsApproved,
IsLockedOut = item.IsLockedOut,
LastLoginDate = item.LastLoginDate.HasValue ? new DateTimeOffset(item.LastLoginDate.Value, TimeSpan.Zero) : null,
LastLockoutDate = item.LastLockoutDate.HasValue ? new DateTimeOffset(item.LastLockoutDate.Value, TimeSpan.Zero) : null,
LastPasswordChangeDate = item.LastPasswordChangeDate.HasValue ? new DateTimeOffset(item.LastPasswordChangeDate.Value, TimeSpan.Zero) : null,
Kind = item.Kind,
Variants = [new MemberVariantResponseModel { Name = item.Name ?? string.Empty }],
Values = [],
MemberType = new MemberTypeReferenceResponseModel
{
Id = item.MemberTypeKey ?? Guid.Empty,
Icon = item.MemberTypeIcon ?? string.Empty,
},
};
private MemberItemResponseModel CreateItemResponseModel<T>(T entity)
where T : ITreeEntity
=> new()
=> new MemberItemResponseModel
{
Id = entity.Key,
MemberType = _umbracoMapper.Map<MemberTypeReferenceResponseModel>(entity)!,
Variants = CreateVariantsItemResponseModels(entity),
Kind = GetMemberKind(entity.Key),
Kind = GetMemberKind(entity.Key)
};
private static IEnumerable<VariantItemResponseModel> CreateVariantsItemResponseModels(ITreeEntity entity)
=>
[
=> new[]
{
new VariantItemResponseModel
{
Name = entity.Name ?? string.Empty,
Culture = null,
Culture = null
}
];
};
private async Task<MemberResponseModel> RemoveSensitiveDataAsync(IMember member, MemberResponseModel responseModel)
{
@@ -1,12 +1,12 @@
using Microsoft.Extensions.DependencyInjection;
using Umbraco.Cms.Api.Management.ViewModels;
using Umbraco.Cms.Api.Management.ViewModels.Member.Item;
using Umbraco.Cms.Api.Management.ViewModels.MemberGroup.Item;
using Umbraco.Cms.Api.Management.ViewModels.PublicAccess;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Core.Mapping;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.Entities;
using Umbraco.Cms.Core.Security;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Services.OperationStatus;
using Umbraco.Cms.Web.Common.Security;
@@ -23,10 +23,8 @@ public class PublicAccessPresentationFactory : IPublicAccessPresentationFactory
private readonly IEntityService _entityService;
private readonly IMemberService _memberService;
private readonly IUmbracoMapper _mapper;
private readonly IMemberRoleManager _memberRoleManager;
private readonly IMemberPresentationFactory _memberPresentationFactory;
private readonly IMemberGroupService _memberGroupService;
// TODO (V19): When the obsolete constructor is removed, consider also remove the unused dependency on IMemberRoleManager.
/// <summary>
/// Initializes a new instance of the <see cref="PublicAccessPresentationFactory"/> class.
@@ -36,37 +34,18 @@ public class PublicAccessPresentationFactory : IPublicAccessPresentationFactory
/// <param name="mapper">The Umbraco mapper for mapping entities to response models.</param>
/// <param name="memberRoleManager">The member role manager for resolving member groups.</param>
/// <param name="memberPresentationFactory">The member presentation factory for creating member item response models.</param>
/// <param name="memberGroupService">The member group service for resolving member groups by name.</param>
public PublicAccessPresentationFactory(
IEntityService entityService,
IMemberService memberService,
IUmbracoMapper mapper,
IMemberRoleManager memberRoleManager,
IMemberPresentationFactory memberPresentationFactory,
IMemberGroupService memberGroupService)
{
_entityService = entityService;
_memberService = memberService;
_mapper = mapper;
_memberPresentationFactory = memberPresentationFactory;
_memberGroupService = memberGroupService;
}
[Obsolete("Please use the constructor with all parameters. Scheduled for removal in Umbraco 19.")]
public PublicAccessPresentationFactory(
IEntityService entityService,
IMemberService memberService,
IUmbracoMapper mapper,
IMemberRoleManager memberRoleManager,
IMemberPresentationFactory memberPresentationFactory)
: this(
entityService,
memberService,
mapper,
memberRoleManager,
memberPresentationFactory,
StaticServiceProvider.Instance.GetRequiredService<IMemberGroupService>())
{
_entityService = entityService;
_memberService = memberService;
_mapper = mapper;
_memberRoleManager = memberRoleManager;
_memberPresentationFactory = memberPresentationFactory;
}
/// <inheritdoc/>
@@ -128,15 +107,21 @@ public class PublicAccessPresentationFactory : IPublicAccessPresentationFactory
.Select(_memberPresentationFactory.CreateItemResponseModel)
.ToArray();
// Resolve groups via IMemberGroupService so custom implementations (e.g. backed by an external
// user store) are honoured here, rather than going directly to IMemberRoleManager/IEntityService.
MemberGroupItemResponseModel[] memberGroups = entry.Rules
var allGroups = _memberRoleManager.Roles.Where(x => x.Name != null).ToDictionary(x => x.Name!);
IEnumerable<UmbracoIdentityRole> identityRoles = entry.Rules
.Where(rule => rule.RuleType == Constants.Conventions.PublicAccess.MemberRoleRuleType)
.Select(rule => rule.RuleValue is null ? null : _memberGroupService.GetByName(rule.RuleValue))
.Select(rule =>
rule.RuleValue is not null && allGroups.TryGetValue(rule.RuleValue, out UmbracoIdentityRole? memberRole)
? memberRole
: null)
.WhereNotNull()
.Select(group => _mapper.Map<MemberGroupItemResponseModel>(group)!)
.ToArray();
IEnumerable<IEntitySlim> groupsEntities = identityRoles.Any()
? _entityService.GetAll(UmbracoObjectTypes.MemberGroup, identityRoles.Select(x => Convert.ToInt32(x.Id)).ToArray())
: Enumerable.Empty<IEntitySlim>();
MemberGroupItemResponseModel[] memberGroups = groupsEntities.Select(x => _mapper.Map<MemberGroupItemResponseModel>(x)!).ToArray();
var responseModel = new PublicAccessResponseModel
{
Members = members,
@@ -1,6 +1,5 @@
using Umbraco.Cms.Api.Management.ViewModels;
using Umbraco.Cms.Api.Management.ViewModels.RedirectUrlManagement;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Routing;
@@ -34,12 +33,12 @@ public class RedirectUrlPresentationFactory : IRedirectUrlPresentationFactory
{
var destinationUrl = source.ContentId > 0
? _publishedUrlProvider.GetUrl(source.ContentId, culture: source.Culture)
: Constants.Routing.Unroutable;
: "#";
var originalUrl = _publishedUrlProvider.GetUrlFromRoute(source.ContentId, source.Url, source.Culture);
// Even if the URL could not be extracted from the route, if we have a path as a the route for the original URL, we should display it.
if (originalUrl == Constants.Routing.Unroutable && source.Url.StartsWith('/'))
if (originalUrl == "#" && source.Url.StartsWith('/'))
{
originalUrl = source.Url;
}
@@ -39,7 +39,6 @@ public class UserPresentationFactory : IUserPresentationFactory
private readonly IBackOfficeExternalLoginProviders _externalLoginProviders;
private readonly SecuritySettings _securitySettings;
private readonly Dictionary<Type, IPermissionPresentationMapper> _permissionPresentationMappersByType;
private readonly IContentPermissionService _contentPermissionService;
/// <summary>
/// Initializes a new instance of the <see cref="UserPresentationFactory"/> class.
@@ -55,50 +54,6 @@ public class UserPresentationFactory : IUserPresentationFactory
/// <param name="securitySettings">Provides access to security-related configuration settings.</param>
/// <param name="externalLoginProviders">Manages back office external login providers.</param>
/// <param name="permissionPresentationMappers">Collection of mappers for permission presentation models.</param>
/// <param name="contentPermissionService">Service for managing content permissions.</param>
public UserPresentationFactory(
IEntityService entityService,
AppCaches appCaches,
MediaFileManager mediaFileManager,
IImageUrlGenerator imageUrlGenerator,
IUserGroupPresentationFactory userGroupPresentationFactory,
IAbsoluteUrlBuilder absoluteUrlBuilder,
IEmailSender emailSender,
IPasswordConfigurationPresentationFactory passwordConfigurationPresentationFactory,
IOptionsSnapshot<SecuritySettings> securitySettings,
IBackOfficeExternalLoginProviders externalLoginProviders,
IEnumerable<IPermissionPresentationMapper> permissionPresentationMappers,
IContentPermissionService contentPermissionService)
{
_entityService = entityService;
_appCaches = appCaches;
_mediaFileManager = mediaFileManager;
_imageUrlGenerator = imageUrlGenerator;
_userGroupPresentationFactory = userGroupPresentationFactory;
_emailSender = emailSender;
_passwordConfigurationPresentationFactory = passwordConfigurationPresentationFactory;
_externalLoginProviders = externalLoginProviders;
_securitySettings = securitySettings.Value;
_absoluteUrlBuilder = absoluteUrlBuilder;
_permissionPresentationMappersByType = permissionPresentationMappers.ToDictionary(x => x.PresentationModelToHandle);
_contentPermissionService = contentPermissionService;
}
/// <summary>
/// Initializes a new instance of the <see cref="UserPresentationFactory"/> class.
/// </summary>
/// <param name="entityService">Service for accessing and managing entities.</param>
/// <param name="appCaches">Provides application-level caching functionality.</param>
/// <param name="mediaFileManager">Manages media file storage and retrieval.</param>
/// <param name="imageUrlGenerator">Generates URLs for images.</param>
/// <param name="userGroupPresentationFactory">Factory for creating user group presentation models.</param>
/// <param name="absoluteUrlBuilder">Builds absolute URLs for resources.</param>
/// <param name="emailSender">Handles sending emails.</param>
/// <param name="passwordConfigurationPresentationFactory">Factory for password configuration presentation models.</param>
/// <param name="securitySettings">Provides access to security-related configuration settings.</param>
/// <param name="externalLoginProviders">Manages back office external login providers.</param>
/// <param name="permissionPresentationMappers">Collection of mappers for permission presentation models.</param>
[Obsolete("Please use the constructor with all parameters. Scheduled for removal in Umbraco 19.")]
public UserPresentationFactory(
IEntityService entityService,
AppCaches appCaches,
@@ -111,20 +66,18 @@ public class UserPresentationFactory : IUserPresentationFactory
IOptionsSnapshot<SecuritySettings> securitySettings,
IBackOfficeExternalLoginProviders externalLoginProviders,
IEnumerable<IPermissionPresentationMapper> permissionPresentationMappers)
: this(
entityService,
appCaches,
mediaFileManager,
imageUrlGenerator,
userGroupPresentationFactory,
absoluteUrlBuilder,
emailSender,
passwordConfigurationPresentationFactory,
securitySettings,
externalLoginProviders,
permissionPresentationMappers,
StaticServiceProvider.Instance.GetRequiredService<IContentPermissionService>())
{
_entityService = entityService;
_appCaches = appCaches;
_mediaFileManager = mediaFileManager;
_imageUrlGenerator = imageUrlGenerator;
_userGroupPresentationFactory = userGroupPresentationFactory;
_emailSender = emailSender;
_passwordConfigurationPresentationFactory = passwordConfigurationPresentationFactory;
_externalLoginProviders = externalLoginProviders;
_securitySettings = securitySettings.Value;
_absoluteUrlBuilder = absoluteUrlBuilder;
_permissionPresentationMappersByType = permissionPresentationMappers.ToDictionary(x => x.PresentationModelToHandle);
}
/// <inheritdoc/>
@@ -274,9 +227,7 @@ public class UserPresentationFactory : IUserPresentationFactory
ISet<ReferenceByIdModel> documentStartNodeKeys = GetKeysFromIds(contentStartNodeIds, UmbracoObjectTypes.Document);
HashSet<IPermissionPresentationModel> permissions = GetAggregatedGranularPermissions(user, presentationGroups);
ISet<string> fallbackPermissions = await _contentPermissionService.FilterFallbackPermissionsAsync(
user,
presentationGroups.SelectMany(x => x.FallbackPermissions).ToHashSet());
var fallbackPermissions = presentationGroups.SelectMany(x => x.FallbackPermissions).ToHashSet();
var hasAccessToAllLanguages = presentationGroups.Any(x => x.HasAccessToAllLanguages);
@@ -389,15 +340,4 @@ public class UserPresentationFactory : IUserPresentationFactory
private static bool HasRootAccess(IEnumerable<int>? startNodeIds)
=> startNodeIds?.Contains(Constants.System.Root) is true;
/// <inheritdoc/>
public Task<UserUpdateProfileModel> CreateUpdateProfileModelAsync(UpdateCurrentUserRequestModel updateModel)
{
var model = new UserUpdateProfileModel
{
LanguageIsoCode = updateModel.LanguageIsoCode
};
return Task.FromResult(model);
}
}
@@ -41,7 +41,6 @@ public class ItemTypeMapDefinition : IMapDefinition
mapper.Define<IMediaType, MediaTypeItemResponseModel>((_, _) => new MediaTypeItemResponseModel(), Map);
mapper.Define<MediaTypeFileExtensionMatchResult, AllowedMediaTypeItemResponseModel>((_, _) => new AllowedMediaTypeItemResponseModel(), Map);
mapper.Define<IEntitySlim, MemberGroupItemResponseModel>((_, _) => new MemberGroupItemResponseModel(), Map);
mapper.Define<IMemberGroup, MemberGroupItemResponseModel>((_, _) => new MemberGroupItemResponseModel(), Map);
mapper.Define<ITemplate, TemplateItemResponseModel>((_, _) => new TemplateItemResponseModel { Alias = string.Empty }, Map);
mapper.Define<IMemberType, MemberTypeItemResponseModel>((_, _) => new MemberTypeItemResponseModel(), Map);
mapper.Define<IRelationType, RelationTypeItemResponseModel>((_, _) => new RelationTypeItemResponseModel(), Map);
@@ -106,13 +105,6 @@ public class ItemTypeMapDefinition : IMapDefinition
target.Id = source.Key;
}
// Umbraco.Code.MapAll -Flags
private static void Map(IMemberGroup source, MemberGroupItemResponseModel target, MapperContext context)
{
target.Name = source.Name ?? string.Empty;
target.Id = source.Key;
}
// Umbraco.Code.MapAll -Flags
private static void Map(ITemplate source, TemplateItemResponseModel target, MapperContext context)
{
@@ -48,7 +48,7 @@ public class MemberMapDefinition : ContentMapDefinition<IMember, MemberValueResp
public void DefineMaps(IUmbracoMapper mapper)
=> mapper.Define<IMember, MemberResponseModel>((_, _) => new MemberResponseModel(), Map);
// Umbraco.Code.MapAll -IsTwoFactorEnabled -Groups -Kind -Flags -ProfileData
// Umbraco.Code.MapAll -IsTwoFactorEnabled -Groups -Kind -Flags
private void Map(IMember source, MemberResponseModel target, MapperContext context)
{
target.Id = source.Key;
@@ -19,30 +19,16 @@ namespace Umbraco.Cms.Api.Management.Mapping.Permissions;
/// </remarks>
public class DocumentPermissionMapper : IPermissionPresentationMapper, IPermissionMapper
{
private readonly Lazy<IContentPermissionService> _contentPermissionService;
private readonly Lazy<IEntityService> _entityService;
private readonly Lazy<IUserService> _userService;
/// <summary>
/// Initializes a new instance of the <see cref="DocumentPermissionMapper"/> class.
/// </summary>
/// <param name="entityService">The entity service.</param>
/// <param name="userService">The user service.</param>
/// <param name="contentPermissionService">The content permission service.</param>
// TODO (V19): Remove the entityService and userService parameters as they are not used in the current implementation.
public DocumentPermissionMapper(
Lazy<IEntityService> entityService,
Lazy<IUserService> userService,
Lazy<IContentPermissionService> contentPermissionService) => _contentPermissionService = contentPermissionService;
/// <summary>
/// Initializes a new instance of the <see cref="DocumentPermissionMapper"/> class.
/// </summary>
[Obsolete("Please use the constructor with all parameters. Scheduled for removal in Umbraco 19.")]
public DocumentPermissionMapper(Lazy<IEntityService> entityService, Lazy<IUserService> userService)
: this(
entityService,
userService,
new Lazy<IContentPermissionService>(StaticServiceProvider.Instance.GetRequiredService<IContentPermissionService>))
{
_entityService = entityService;
_userService = userService;
}
/// <inheritdoc/>
@@ -124,18 +110,25 @@ public class DocumentPermissionMapper : IPermissionPresentationMapper, IPermissi
.Distinct()
.ToArray();
// Resolve permissions through IContentPermissionService so custom implementations are respected.
IEnumerable<NodePermissions> permissions = _contentPermissionService.Value
.GetPermissionsAsync(user, documentKeysWithGranularPermissions)
.GetAwaiter()
.GetResult();
// Batch retrieve all documents by their keys.
var documents = _entityService.Value.GetAll<IContent>(documentKeysWithGranularPermissions)
.ToDictionary(doc => doc.Key, doc => doc.Path);
foreach (NodePermissions nodePermission in permissions)
// Iterate through each document key that has granular permissions.
foreach (Guid documentKey in documentKeysWithGranularPermissions)
{
// Retrieve the path from the pre-fetched documents.
if (!documents.TryGetValue(documentKey, out var path) || string.IsNullOrEmpty(path))
{
continue;
}
// With the path we can call the same logic as used server-side for authorizing access to resources.
EntityPermissionSet permissionsForPath = _userService.Value.GetPermissionsForPath(user, path);
yield return new DocumentPermissionPresentationModel
{
Document = new ReferenceByIdModel(nodePermission.NodeKey),
Verbs = nodePermission.Permissions,
Document = new ReferenceByIdModel(documentKey),
Verbs = permissionsForPath.GetAllPermissions(),
};
}
}
File diff suppressed because it is too large Load Diff
@@ -1,12 +1,8 @@
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using Umbraco.Cms.Api.Management.Controllers.Security;
using Umbraco.Cms.Api.Management.ServerEvents;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Configuration.Models;
using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Web.Common.Routing;
using Umbraco.Extensions;
@@ -16,36 +12,26 @@ namespace Umbraco.Cms.Api.Management.Routing;
/// <summary>
/// Creates routes for the back office area.
/// </summary>
public sealed class BackOfficeAreaRoutes : SignalRRoutesBase, IAreaRoutes
public sealed class BackOfficeAreaRoutes : IAreaRoutes
{
/// <summary>
/// Initializes a new instance of the <see cref="BackOfficeAreaRoutes" /> class.
/// </summary>
[Obsolete("Please use the constructor with all parameters. Scheduled for removal in Umbraco 19.")]
public BackOfficeAreaRoutes(IRuntimeState runtimeState)
: this(
runtimeState,
StaticServiceProvider.Instance.GetRequiredService<IOptions<SignalRSettings>>())
{
}
private readonly IRuntimeState _runtimeState;
/// <summary>
/// Initializes a new instance of the <see cref="BackOfficeAreaRoutes" /> class.
/// </summary>
public BackOfficeAreaRoutes(IRuntimeState runtimeState, IOptions<SignalRSettings> signalRSettings)
: base(runtimeState, signalRSettings)
{
}
public BackOfficeAreaRoutes(IRuntimeState runtimeState)
=> _runtimeState = runtimeState;
/// <inheritdoc />
public void CreateRoutes(IEndpointRouteBuilder endpoints)
{
if (RuntimeState.Level is RuntimeLevel.Install or RuntimeLevel.Upgrade or RuntimeLevel.Upgrading or RuntimeLevel.Run)
if (_runtimeState.Level is RuntimeLevel.Install or RuntimeLevel.Upgrade or RuntimeLevel.Upgrading or RuntimeLevel.Run)
{
MapMinimalBackOffice(endpoints);
endpoints.MapHub<BackofficeHub>(Constants.System.UmbracoPathSegment + Constants.Web.BackofficeSignalRHub, ConfigureHubEndpoint);
endpoints.MapHub<ServerEventHub>(Constants.System.UmbracoPathSegment + Constants.Web.ServerEventSignalRHub, ConfigureHubEndpoint);
endpoints.MapHub<BackofficeHub>(Constants.System.UmbracoPathSegment + Constants.Web.BackofficeSignalRHub);
endpoints.MapHub<ServerEventHub>(Constants.System.UmbracoPathSegment + Constants.Web.ServerEventSignalRHub);
}
}
@@ -1,11 +1,7 @@
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using Umbraco.Cms.Api.Management.Preview;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Configuration.Models;
using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Web.Common.Routing;
@@ -14,29 +10,16 @@ namespace Umbraco.Cms.Api.Management.Routing;
/// <summary>
/// Creates routes for the preview hub
/// </summary>
public sealed class PreviewRoutes : SignalRRoutesBase, IAreaRoutes
public sealed class PreviewRoutes : IAreaRoutes
{
/// <summary>
/// Initializes a new instance of the <see cref="PreviewRoutes"/> class, configuring preview routing based on the application's runtime state.
/// </summary>
/// <param name="runtimeState">An instance representing the current runtime state of the Umbraco application.</param>
[Obsolete("Please use the constructor with all parameters. Scheduled for removal in Umbraco 19.")]
public PreviewRoutes(IRuntimeState runtimeState)
: this(
runtimeState,
StaticServiceProvider.Instance.GetRequiredService<IOptions<SignalRSettings>>())
{
}
private readonly IRuntimeState _runtimeState;
/// <summary>
/// Initializes a new instance of the <see cref="PreviewRoutes"/> class, configuring preview routing based on the application's runtime state.
/// Initializes a new instance of the <see cref="Umbraco.Cms.Api.Management.Routing.PreviewRoutes"/> class, configuring preview routing based on the application's runtime state.
/// </summary>
/// <param name="runtimeState">An instance representing the current runtime state of the Umbraco application.</param>
/// <param name="signalRSettings">The SignalR settings options.</param>
public PreviewRoutes(IRuntimeState runtimeState, IOptions<SignalRSettings> signalRSettings)
: base(runtimeState, signalRSettings)
{
}
public PreviewRoutes(IRuntimeState runtimeState)
=> _runtimeState = runtimeState;
/// <summary>
/// Creates the preview routes on the specified endpoint route builder.
@@ -44,9 +27,9 @@ public sealed class PreviewRoutes : SignalRRoutesBase, IAreaRoutes
/// <param name="endpoints">The endpoint route builder to add routes to.</param>
public void CreateRoutes(IEndpointRouteBuilder endpoints)
{
if (RuntimeState.Level is RuntimeLevel.Install or RuntimeLevel.Upgrade or RuntimeLevel.Upgrading or RuntimeLevel.Run)
if (_runtimeState.Level is RuntimeLevel.Install or RuntimeLevel.Upgrade or RuntimeLevel.Upgrading or RuntimeLevel.Run)
{
endpoints.MapHub<PreviewHub>(GetPreviewHubRoute(), ConfigureHubEndpoint);
endpoints.MapHub<PreviewHub>(GetPreviewHubRoute());
}
}
@@ -58,4 +41,3 @@ public sealed class PreviewRoutes : SignalRRoutesBase, IAreaRoutes
/// </returns>
public string GetPreviewHubRoute() => $"/{Constants.System.UmbracoPathSegment}/{nameof(PreviewHub)}";
}
@@ -1,45 +0,0 @@
using Microsoft.AspNetCore.Http.Connections;
using Microsoft.Extensions.Options;
using Umbraco.Cms.Core.Configuration.Models;
using Umbraco.Cms.Core.Services;
namespace Umbraco.Cms.Api.Management.Routing;
/// <summary>
/// Base class for route definitions that map SignalR hub endpoints,
/// applying shared transport configuration from <see cref="SignalRSettings"/>.
/// </summary>
public abstract class SignalRRoutesBase
{
private readonly SignalRSettings _signalRSettings;
/// <summary>
/// Initializes a new instance of the <see cref="SignalRRoutesBase"/> class.
/// </summary>
/// <param name="runtimeState">The current runtime state of the Umbraco application.</param>
/// <param name="signalRSettings">The SignalR settings options.</param>
protected SignalRRoutesBase(IRuntimeState runtimeState, IOptions<SignalRSettings> signalRSettings)
{
RuntimeState = runtimeState;
_signalRSettings = signalRSettings.Value;
}
/// <summary>
/// Gets the current runtime state of the Umbraco application.
/// </summary>
protected IRuntimeState RuntimeState { get; }
/// <summary>
/// Configures the transport options for a SignalR hub endpoint.
/// When <see cref="SignalRSettings.ClientShouldSkipNegotiation"/> is enabled,
/// restricts the endpoint to WebSocket transport only so clients can skip the negotiate round-trip.
/// </summary>
/// <param name="options">The hub endpoint dispatcher options to configure.</param>
protected void ConfigureHubEndpoint(HttpConnectionDispatcherOptions options)
{
if (_signalRSettings.ClientShouldSkipNegotiation)
{
options.Transports = HttpTransportType.WebSockets;
}
}
}

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