Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
246bb4c33d | ||
|
|
ac811c2624 |
@@ -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.
|
||||
```
|
||||
@@ -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.
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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: |
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 }}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
-135
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
+1
-66
@@ -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)
|
||||
{
|
||||
|
||||
+3
-6
@@ -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);
|
||||
|
||||
-100
@@ -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);
|
||||
}
|
||||
}
|
||||
-102
@@ -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);
|
||||
}
|
||||
}
|
||||
+2
-65
@@ -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,
|
||||
|
||||
+2
-65
@@ -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,
|
||||
|
||||
+10
-43
@@ -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)
|
||||
|
||||
+2
-65
@@ -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,
|
||||
|
||||
+2
-65
@@ -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
-50
@@ -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
-50
@@ -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
-50
@@ -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,
|
||||
|
||||
+3
-6
@@ -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;
|
||||
|
||||
+26
-39
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
+17
-32
@@ -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());
|
||||
|
||||
+11
-33
@@ -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);
|
||||
|
||||
|
||||
+12
-26
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
+3
-14
@@ -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));
|
||||
|
||||
+45
-16
@@ -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));
|
||||
|
||||
+3
-14
@@ -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);
|
||||
|
||||
+99
-107
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
-54
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
+11
-38
@@ -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 });
|
||||
}
|
||||
|
||||
+12
-3
@@ -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
|
||||
|
||||
-64
@@ -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 = "~/";
|
||||
|
||||
+1
-2
@@ -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(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
+12
-988
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
Reference in New Issue
Block a user