diff --git a/.claude/skills/umb-review/SKILL.md b/.claude/skills/umb-review/SKILL.md new file mode 100644 index 000000000000..b0af1bb13bce --- /dev/null +++ b/.claude/skills/umb-review/SKILL.md @@ -0,0 +1,251 @@ +--- +name: umb-review +description: Automated PR code review for Umbraco CMS. Analyzes changed files for intent, impact on consumers, breaking changes, architecture compliance, and code quality. Non-interactive — outputs a full structured review. Use this skill whenever the user asks to review a branch, review a PR, check their changes for issues, analyze a diff, or validate breaking change patterns — even if they don't say "review" explicitly. Does NOT apply to writing new code, fixing bugs, refactoring, explaining architecture, writing tests, or reviewing documentation content. +argument-hint: +--- + +# PR Review - Umbraco CMS + +Automated, non-interactive PR code review. Analyzes changed files for intent, impact on consumers, breaking changes, architecture compliance, and code quality. + +**Do NOT use AskUserQuestion at any point. This skill runs fully autonomously.** + +## Arguments + +- `$ARGUMENTS` - Optional: target branch to diff against (auto-detected from PR, falls back to `origin/main`) + +## Instructions + +### 0. Verify GH CLI is Available + +Run `gh auth status`. If it fails, read `references/gh-cli-setup.md` and present the setup instructions to the user. Do not proceed with the review. + +### 1. Resolve Target Branch + +Determine the target branch for comparison using this priority order: + +1. **Explicit argument**: If `$ARGUMENTS` is provided and non-empty, use it as the target branch +2. **PR target branch**: If no argument, run `gh pr view --json baseRefName --jq '.baseRefName'` to detect the target branch of the current branch's open PR. If a PR exists, use `origin/{baseRefName}` as the target branch. +3. **Fallback**: If no argument and no PR found (command fails or returns empty), default to `origin/main` + +Store the resolved target branch for use in subsequent steps. Log which resolution method was used (e.g., "Target branch: `origin/v18/dev` (from PR #1234)"). + +### 2. Load Review Standards + +#### 2a. Load coding preferences + +Read the coding preferences and code review scoring criteria from: + +- `references/coding-preferences.md` (relative to this skill file) + +Parse and internalize all rules, conventions, scoring categories, and severity definitions. These are your review criteria. + +#### 2b. Load area-specific documentation + +Once the changed file list is known (after step 3a), determine which areas of the codebase are touched and load the relevant documentation. Execute this sub-step between 3a and 3b. This documentation takes precedence over sibling comparison for architectural and pattern validation. + +**Resolution order for each changed file:** + +1. **Find the nearest `CLAUDE.md`** — walk up from the changed file's directory toward the repository root. The first `CLAUDE.md` found is the area guide for that file. Read it. +2. **Read referenced docs** — if the `CLAUDE.md` references documentation files (e.g., a `docs/` directory), use the descriptions in the `CLAUDE.md` to determine which docs are relevant to the type of code being changed, and read those. If unsure, read all referenced docs — the cost of reading is low, the cost of missing a convention is high. +3. **Follow cross-references in loaded docs** — if a loaded doc references another doc as covering a complementary or related concern, and the changed files touch that concern, read the referenced doc too. Repeat until no new relevant cross-references remain. +4. **Check for applicable skills** — review the available skills list. If a skill exists for the type of code being changed, read the skill file to understand the expected patterns, structure, and conventions it enforces. Do NOT invoke the skill — just use it as a reference for what the correct implementation should look like. + +**Store all loaded documentation** for use in step 4. These docs define the authoritative patterns and conventions that the review evaluates against. + +### 3. Gather Changed Files + +#### 3a. Collect file list, stats, and diff + +Run these git commands (where `{target}` is the resolved target branch): + +```bash +git diff {target}...HEAD --name-only --diff-filter=d # changed files (excluding deleted) +git diff {target}...HEAD --stat # line counts per file +git log {target}...HEAD --oneline # commit history +git diff {target}...HEAD # full diff (primary review source) +``` + +**If no changes found**: Output "No changes found between current branch and `{target}`. Nothing to review." and stop. + +#### 3b. Filter out noise files + +From the changed file list, classify each file as **noise** or **reviewable**. + +**Noise files** (skip entirely — do not read, do not review): + +| Pattern | Reason | +| ---------------------------------------------------- | ------------------------------- | +| `*.gen.ts`, `*.gen.cs` | Auto-generated API client code | +| `*.generated.cs`, `*.Designer.cs` (in `Migrations/`) | Auto-generated models/snapshots | +| `*/assets/lang/*.ts` (except `en.ts`) | Non-English translation files | +| `*/mocks/data/*.ts` | Test fixture data | +| `*/dist-cms/*`, `*/storybook-static/*` | Build output | +| `*/TEMP/InMemoryAuto/*` | Runtime-generated models | +| `package-lock.json` | Dependency lock file | +| `appsettings-schema.*.json` | Generated JSON schema | + +Log the skip list: "Skipped {N} noise files: {comma-separated list of filenames}" + +#### 3c. Read reviewable changed files + +Read the full file for every reviewable changed file. + +#### 3d. Track file counts + +Keep track of these numbers for the review output in step 7: total changed files, noise files skipped, and reviewable files read. Also record: distinct production layers touched, distinct project directories, and total lines changed — these feed step 3e. + +#### 3e. Assess PR complexity + +Follow the procedure in `references/complexity-assessment.md`. Store the triggered dimensions and suggestions for step 7. + +#### 3f. Classify PR scope + +Classify the PR to determine which review steps are relevant: + +| Classification | Condition | Effect | +| --------------- | ------------------------------------------------------------------------------- | ------------------------------------------------------------------------------- | +| **Gen-only** | All reviewable files are `gen.ts` | Skip steps 5 and 6; step 4 reviews impact on other code only | +| **Docs-only** | All reviewable files are `.md` | Skip steps 5 and 6; step 4 reviews intent and readability only | +| **Test-only** | All reviewable files are in `tests/` | Skip steps 5 and 6; step 4 reviews intent, code quality, and test coverage only | +| **Config-only** | All reviewable files are `.csproj`, `.props`, `.json` config, or CI/build files | Skip step 5; step 6 checks dependency version changes only | +| **Standard** | Anything else | No skips — run all steps | + +### 4. Raw Code Review + +Review each changed file holistically. Think like a senior developer reading a colleague's PR. Note all findings without worrying about format or severity yet. + +#### 4a. Read and reason about each file + +For each changed file, reason about: What does this code do? Is it correct? What's missing — validation, error handling, notifications, cleanup, edge cases? Could this break anything for consumers? + +#### 4b. Validate against documentation and patterns + +Use a **docs-first** approach: classify the code by what it does, check it against documented conventions, and only fall back to sibling comparison when docs don't cover the pattern. + +**Step 1 — Determine the correct approach from documentation, then check whether the PR matches** + +A PR is a proposed solution, not the source of truth. This step has two parts that must happen in order — do not start part B until part A is complete. + +**Part A — Before validating/judging the implementation**, determine what the correct approach is for each new class or file based on what it does. Use the documentation loaded in step 2b to identify the expected base classes, patterns, and conventions. Write down the expected approach. Classify based on what the code does, not based on what neighboring files look like. + +**Part B — Now compare the PR's implementation** against the expected approach from Part A. If it deviates from the documented approach, flag it. If the documentation specifies reference examples, read those examples to verify the implementation matches. + +**Pattern match is the leading finding.** If the documentation defines a pattern that fits what the code does, the first and most important finding is whether the code follows that pattern. + +**Step 2 — Fall back to sibling comparison** + +If the documentation does not cover the specific pattern, or for cross-cutting concerns not addressed in docs, fall back to sibling comparison: + +1. **New method on existing class/interface**: Grep for the most similar existing method on the same class using `-A 80` to capture the full method body (e.g., `UpdateCurrentUserAsync` → grep for `UpdateAsync` in the same file with `-A 80`). Compare line by line for missing cross-cutting concerns: notifications/events, validation, scoping, authorization, error handling, audit logging. +2. **New TS class**: Grep for siblings by base class (`extends {BaseClass}`) or by interface (`implements {Interface}`) or by name suffix (e.g., `CurrentUserController` → grep for `UserController`). Compare for missing concerns. +3. **New CS class**: Grep for siblings by base class (`class {ClassName} : {BaseClass}`) or by interface (`class {ClassName} : {Interface}`) or by name suffix (e.g., `ManagementApiComposer` → grep for `ApiComposer`). Compare for missing concerns. + +**Important:** Sibling comparison validates cross-cutting concerns, but it must not override documented conventions. If a sibling deviates from documented patterns, that sibling is wrong — do not copy its deviation. + +Store your raw findings — they feed into step 7. + +### 5. Impact Analysis + +**Skip this step if PR scope is docs-only, test-only, or config-only.** + +Follow the procedure in `references/impact-analysis.md`. + +### 6. Breaking Changes Check + +**Skip this step if PR scope is docs-only or test-only. If config-only, only check for dependency version changes that could break consumers.** + +Follow the procedure in `references/breaking-changes.md`. + +### 7. Consolidate and Output Review + +Merge findings from step 4 (raw review), step 5 (impact analysis), and step 6 (breaking changes). For each finding, assign severity (Critical/Important/Suggestion) and verify it relates to changed code — not pre-existing issues. Before outputting, drop any finding about whitespace, blank lines, formatting, or comment wording. Then present the review in this exact format: + +```markdown +## PR Review + +**Target:** `{target_branch}` · **Based on commit:** `{head_sha}` +[If any skipped files, append: · **Skipped:** {skipped} files out of {total} total] +[If step 3f classification is not "Standard", append: · **Classified as:** {classification}] + +[1–2 sentences: what this PR accomplishes , keep it as short as possible, only highlight the primary essence.] + +- **Modified public API:** {changed existing interfaces/types/classes/methods} + [Omit bullet if none] +- **Affected implementations (outside this PR):** {interfaces/types/classes/methods using modified public API} + [Omit bullet if none] +- **Breaking changes:** {violations with specifics} + [Omit bullet if none] +- **Other changes:** {changes not listed above that an Umbraco user, plugin developer, or API consumer would notice — e.g., behavior changes, default value changes, error message changes, new configuration options, removed functionality. Exclude internal renames, formatting, and private implementation details.} + [Omit bullet if none] + +[If step 3e triggered any dimensions, insert this block. Omit entirely if nothing triggered:] + +> [!NOTE] +> **Complexity advisory** — This PR may benefit from splitting. +> +> - **{Dimension}:** {Explanation and concrete split suggestion from step 3e} +> [one bullet per triggered dimension] +> +> _This is an observation, not a blocker. The full review follows below._ + +--- + +### Critical + +[Must fix before merge — security vulnerabilities, data loss, broken functionality, breaking changes without proper patterns] + +- **`{file}:{line}`**: {problem} → {fix} + +[Omit section if none] + +### Important + +[Should fix — performance issues, missing tests, architectural violations, pattern misuse] + +- **`{file}:{line}`**: {observation} → {suggestion} + +[Omit section if none] + +### Suggestions + +[Nice to have — readability, minor refactoring, alternative approaches] + +- **`{file}:{line}`**: {detail} + +[Omit section if none] + +--- + +[One of:] + +## Approved + +This looks good to be merged as-is, but please do a manual sanity check and testing before merging. + +## Approved with Suggestions for improvement + +Good to go, but please carefully consider the importance of the suggestions. + +## Request Changes + +Critical and important issues must be addressed first. + +## Needs re-work + +This is in such a bad state that the feedback of this review is not sufficient to guide improvements, the PR cannot be approved. +``` + +**Guidelines for the review output:** + +— When reporting information, be extremely concise and sacrifice grammar for sake of concision. + +- Only review code that was changed in the diff — pre-existing issues are out of scope. Focus on what compilers and linters cannot catch: behavioral side-effects (e.g., a changed default alters runtime behavior for consumers), architectural violations (e.g., a new dependency breaks layering), breaking changes for external consumers of the public API, and security implications. Leave type errors, missing imports, and broken references to CI. +- Be specific — always reference file and line number +- Explain WHY something is an issue, not just WHAT, but avoid stating the obvious. +- For complex matters, provide concrete fix suggestions, including code snippets when helpful +- Keep it constructive — the goal is to help, not gatekeep +- Don't repeat the same finding for every occurrence — mention it once and note "same pattern in {other files}" +- Focus on substantive issues only. Do NOT flag purely cosmetic or stylistic concerns. Specifically, never flag: code formatting or whitespace, comment grammar or wording, redundant-but-harmless syntax (e.g., optional chaining after a truthiness check), code duplication that doesn't cause bugs, or HTML template cosmetics. The only exception is when a stylistic issue has a concrete impact on performance or rendering. Note: missing JSDoc/documentation on public or exported APIs is a substantive finding (per coding preferences), not a cosmetic one — flag it as a Suggestion. +- For breaking changes, reference the specific pattern from the CLAUDE.md that should be applied +- Do not suggest changes that would themselves introduce breaking changes. If a suggestion would alter public API surface (e.g., changing return types, renaming public members), it is not appropriate for a PR targeting `main` within a major version. Only suggest non-breaking alternatives. diff --git a/.claude/skills/umb-review/evals/evals.json b/.claude/skills/umb-review/evals/evals.json new file mode 100644 index 000000000000..538bb548eb99 --- /dev/null +++ b/.claude/skills/umb-review/evals/evals.json @@ -0,0 +1,97 @@ +{ + "skill_name": "umb-review", + "evals": [ + { + "id": 0, + "name": "pr-22214-large-frontend-refactor", + "prompt": "Review the changes in PR #22214 (branch origin/pr/22214 targeting main). This is a large frontend refactor migrating create entity actions to use entityCreateOptionAction extensions, with deprecations.", + "expected_output": "A structured review that identifies frontend deprecation patterns, flags the large PR complexity, handles 75+ files correctly, checks for breaking changes in exported components, and produces the correct output format.", + "pr_number": 22214, + "pr_branch": "origin/pr/22214", + "base_branch": "origin/main", + "files": [], + "assertions": [ + {"id": "deprecation-patterns-noted", "text": "Review identifies deprecation patterns (@deprecated, UmbDeprecation)"}, + {"id": "frontend-breaking-change-awareness", "text": "Checks frontend-specific breaking changes (exports, custom elements) not just backend"}, + {"id": "file-references-present", "text": "Findings reference specific files with line numbers"}, + {"id": "no-false-critical-on-deprecations", "text": "Properly deprecated code is NOT flagged as Critical breaking change"}, + {"id": "no-stylistic-nitpicks", "text": "Review does not flag purely cosmetic/stylistic issues (formatting, whitespace, naming conventions, comment grammar, code style preferences) unless they affect performance or rendering. Missing JSDoc on new public APIs is NOT a stylistic issue — it is a legitimate finding."}, + {"id": "manifest-alias-rename-detected", "text": "Alias renames (CreateOptions → Create) flagged as Critical breaking change"}, + {"id": "non-exported-deletions-dismissed", "text": "Deleted action classes NOT flagged as breaking (verified against package.json exports)"}, + {"id": "noise-files-filtered", "text": "Does not review noise files (generated files, lock files, etc.)"}, + {"id": "complexity-advisory-triggers", "text": "Review includes a complexity/split advisory for the large 75+ file scope"} + ] + }, + { + "id": 1, + "name": "pr-21672-small-frontend-bugfix", + "prompt": "Review the changes in PR #21672 (branch origin/pr/21672 targeting main). This is a small 4-file frontend bugfix implementing tab validation badges in the block editor.", + "expected_output": "A clean review that correctly identifies this as a small focused bugfix, avoids false positives, and either approves or approves with minor suggestions.", + "pr_number": 21672, + "pr_branch": "origin/pr/21672", + "base_branch": "origin/main", + "files": [], + "assertions": [ + {"id": "complexity-advisory-absent", "text": "Review does NOT include a complexity/split advisory"}, + {"id": "no-false-breaking-changes", "text": "Review does not flag breaking changes"}, + {"id": "proportionate-verdict", "text": "Verdict is 'Request Changes'"}, + {"id": "concise-review", "text": "Review output is under 200 lines"}, + {"id": "no-stylistic-nitpicks", "text": "Review does not flag purely cosmetic/stylistic issues (formatting, whitespace, naming conventions, comment grammar, code style preferences) unless they affect performance or rendering. Missing JSDoc on new public APIs is NOT a stylistic issue — it is a legitimate finding."} + ] + }, + { + "id": 2, + "name": "pr-22217-small-backend-webhook", + "prompt": "Review the changes in PR #22217 (branch origin/pr/22217 targeting v18/dev). This is a tiny 3-file backend change to the default webhook payload type.", + "expected_output": "A concise review that correctly resolves v18/dev as target branch, handles the small change proportionately, and considers the behavioral impact of changing a default value.", + "pr_number": 22217, + "pr_branch": "origin/pr/22217", + "base_branch": "origin/v18/dev", + "files": [], + "assertions": [ + {"id": "correct-target-branch", "text": "Review references 'v18/dev' as the target branch (not 'main')"}, + {"id": "default-value-change-noted", "text": "Review discusses the behavioral impact of changing the default payload type"}, + {"id": "proportionate-review", "text": "Review output is under 150 lines"}, + {"id": "no-stylistic-nitpicks", "text": "Review does not flag purely cosmetic/stylistic issues (formatting, whitespace, naming conventions, comment grammar, code style preferences) unless they affect performance or rendering. Missing JSDoc on new public APIs is NOT a stylistic issue — it is a legitimate finding."}, + {"id": "ignores-preexisting-issues", "text": "Does NOT flag the ~30 builder extension methods with Legacy defaults (pre-existing, not changed in the PR)"}, + {"id": "side-effect-detection", "text": "Flags stale WebhookSettings.cs docs as a side-effect of the constant value change"}, + {"id": "consumer-identification", "text": "Identifies affected consumers outside the PR (WebhookSettings, UmbracoBuilder, or WebhookEventCollectionBuilderExtensions)"} + ] + }, + { + "id": 3, + "name": "pr-22268-frontend-feature-workspace-modal", + "prompt": "Review the changes in PR #22268 (branch origin/pr/22268 targeting main). This is a 29-file frontend feature adding a current user workspace modal.", + "expected_output": "A review of a medium-sized new feature PR. Should assess the new code for architectural compliance, check for breaking changes (new exports, custom elements), and evaluate code quality without flagging pre-existing issues.", + "pr_number": 22268, + "pr_branch": "origin/pr/22268", + "base_branch": "origin/main", + "files": [], + "assertions": [ + {"id": "complexity-advisory-triggers", "text": "Review includes a complexity/split advisory (3 layers: Core, API, Frontend across 27+ files)"}, + {"id": "breaking-changes-on-interface-additions", "text": "Flags new interface methods without default implementations as breaking changes (Pattern 3)"}, + {"id": "no-stylistic-nitpicks", "text": "Review does not flag purely cosmetic/stylistic issues unless they affect performance or rendering. Missing JSDoc on new public APIs is NOT a stylistic issue — it is a legitimate finding."}, + {"id": "diff-scoped", "text": "All findings reference code that was changed in the diff, not pre-existing issues"}, + {"id": "new-feature-assessed", "text": "Review assesses the new feature's architecture, patterns, or integration approach — not just absence of bugs"}, + {"id": "no-false-notification-finding", "text": "Review does NOT flag UpdateCurrentUserAsync as missing UserSavingNotification/UserSavedNotification — the sibling UpdateAsync also does not publish these notifications, so flagging their absence would be a false positive"} + ] + }, + { + "id": 4, + "name": "pr-22215-frontend-architecture-violation", + "prompt": "Review the changes in PR #22215 (branch origin/pr/22215 targeting main). This is a 2-file frontend feature adding user management to the user group workspace.", + "expected_output": "A review that catches the architecture violation: the workspace context directly imports and calls UserService and UserGroupService (generated API clients) instead of going through a repository. In the Umbraco backoffice, workspace contexts access data via repositories, not by calling API services directly. The review should flag this as a significant architecture issue and request changes.", + "pr_number": 22215, + "pr_branch": "origin/pr/22215", + "base_branch": "origin/main", + "files": [], + "assertions": [ + {"id": "service-bypass-detected", "text": "Review flags that the workspace context directly imports/calls UserService or UserGroupService instead of using a repository"}, + {"id": "repository-pattern-recommended", "text": "Review recommends using the repository pattern (going through a repository/data-source layer) rather than calling API services directly from the workspace context"}, + {"id": "verdict-request-changes", "text": "Verdict is 'Request Changes' (the architecture violation warrants requesting changes, not just approving with suggestions)"}, + {"id": "no-stylistic-nitpicks", "text": "Review does not flag purely cosmetic/stylistic issues (formatting, whitespace, naming conventions, comment grammar, code style preferences) unless they affect performance or rendering. Missing JSDoc on new public APIs is NOT a stylistic issue — it is a legitimate finding."}, + {"id": "no-false-breaking-changes", "text": "Review does not flag breaking changes (this PR only adds new code, no public API is removed or modified)"} + ] + } + ] +} diff --git a/.claude/skills/umb-review/references/breaking-changes.md b/.claude/skills/umb-review/references/breaking-changes.md new file mode 100644 index 000000000000..052867d8598c --- /dev/null +++ b/.claude/skills/umb-review/references/breaking-changes.md @@ -0,0 +1,249 @@ +# Breaking Changes Reference + +This document describes how to detect and validate breaking changes during PR review. It covers both backend (.NET) and frontend (TypeScript/Lit) patterns. + +--- + +## Version Detection + +**Always read `version.json`** at the repository root to determine the current major version. This drives the obsolete removal target calculation: + +- Current major version: read from `version.json` → `version` field (e.g., `"17.4.0-rc"` → major version `17`) +- Obsolete removal target: `current + 2` (e.g., if current is 17, removal is scheduled for Umbraco 19) +- Format: `[Obsolete("... Scheduled for removal in Umbraco {current+2}.")]` + +--- + +## Backend (.NET) Breaking Changes + +### What Constitutes a Breaking Change + +Any of these on a `public` or `protected` member: + +- Removing or renaming a class, interface, struct, record, or enum +- Removing or renaming a method, property, or field +- Changing a method signature (parameters, return type) +- Adding required parameters to an existing method +- Adding methods to a public interface (without default implementation) +- Changing a constructor signature on a public class +- Removing or changing enum values +- Changing type hierarchy (base class, implemented interfaces) + +### Pattern 1: Obsolete Constructor + StaticServiceProvider + +When a public class needs new dependencies, the existing constructor must be preserved. + +**Correct pattern:** + +```csharp +[Obsolete("Please use the constructor with all parameters. Scheduled for removal in Umbraco 19.")] +public MyService(IDependencyA depA) + : this( + depA, + StaticServiceProvider.Instance.GetRequiredService()) +{ +} + +public MyService(IDependencyA depA, IDependencyB depB) +{ + _depA = depA; + _depB = depB; +} +``` + +**Validation checklist:** + +- [ ] Old constructor has `[Obsolete]` attribute with correct removal version +- [ ] Old constructor calls new constructor via `: this(...)` +- [ ] `StaticServiceProvider.Instance.GetRequiredService()` used for new params only +- [ ] DI registration uses the NEW constructor (old is for external consumers only) +- [ ] Removal version is `{current_major + 2}` + +**Common mistakes to flag:** + +- Removing the old constructor entirely (breaking change!) +- Old constructor NOT calling new constructor (code duplication) +- Wrong removal version in `[Obsolete]` +- Missing `StaticServiceProvider` resolution for new dependencies +- DI registration still using the old constructor + +### Pattern 2: Obsolete Method + New Overload + +When a method signature needs to change, add the new overload and obsolete the old. + +**Correct pattern:** + +```csharp +[Obsolete("Use the overload taking all parameters. Scheduled for removal in Umbraco 19.")] +public void DoThing(string name) + => DoThing(name, extraParam: null); + +public void DoThing(string name, string? extraParam) +{ + // Real implementation here +} +``` + +**Validation checklist:** + +- [ ] Old method has `[Obsolete]` attribute with correct removal version +- [ ] Old method calls new method, providing defaults for new parameters +- [ ] All internal callers updated to use the new method +- [ ] No internal code references the obsolete method (except the delegation) + +### Pattern 3: Default Interface Implementation + +When adding methods to a public interface, provide a default implementation. + +**Correct pattern:** + +```csharp +public interface IMyService +{ + void ExistingMethod(); + + // New method with default implementation + void NewMethod(string param) + => ExistingMethod(); // delegate to existing if possible +} +``` + +**Strategies for defaults (in order of preference):** + +1. Use existing interface methods to satisfy the contract +2. Return a sensible default (empty collection, null, etc.) +3. Throw `NotImplementedException` if no reasonable default exists + +**Validation checklist:** + +- [ ] New interface method has a default implementation +- [ ] TODO comment present: `// TODO (V{next-major}): Remove the default implementation when {obsolete method} is removed.` +- [ ] Default implementation is functionally correct (even if not optimal) +- [ ] If `StaticServiceProvider` is used in default impl, noted as temporary + +### Obsolete Attribute Validation + +For any `[Obsolete]` attribute found in changed code: + +1. **Format**: Must contain `"Scheduled for removal in Umbraco {version}."` +2. **Version**: Must be `current_major + 2` (read from `version.json`) +3. **Pragma**: Where obsolete members must call each other, `#pragma warning disable CS0618` / `#pragma warning restore CS0618` must be present + +### Internal Caller Check + +After finding obsolete patterns, verify: + +- Search the codebase for usages of the obsolete member +- **No internal code** (inside `src/`) should reference obsolete members +- Only the obsolete member's own delegation (calling the new version) is acceptable +- External consumers (outside the repo) get the deprecation period to migrate + +--- + +## Frontend (TypeScript/Lit) Breaking Changes + +The backoffice is published as `@umbraco-cms/backoffice` with 140+ named exports. Plugin developers depend on this public API surface. + +**Critical frontend rule (does not apply to backend .NET where `public`/`protected` visibility determines the API surface): only symbols reachable through the `package.json` `exports` field are public API.** Anything not exported — whether classes, functions, constants, types, or entire files — is an internal implementation detail, even if other internal code imports it. Removing or changing unexported frontend symbols is not a breaking change. Before flagging a frontend deletion or rename as breaking, verify the symbol is reachable via `package.json` exports. If it is not, do not flag it. + +### Custom Elements (Web Components) + +**Breaking changes:** + +- Renaming or removing a registered custom element tag (`umb-*`) +- Removing elements from `HTMLElementTagNameMap` +- Removing or changing `@property()` decorated fields on exported components +- Removing event emissions (checked via `this.dispatchEvent`) +- Removing CSS custom properties (`@cssprop` in JSDoc) +- Removing CSS parts (`@csspart` in JSDoc) + +**How to detect:** + +- Check diff for removed `@customElement('umb-...')` decorators +- Check diff for removed `@property()` fields on exported components +- Check diff for removed entries in `HTMLElementTagNameMap` declarations + +### Exported Types/Interfaces + +**Breaking changes:** + +- Removing exports from `package.json` `exports` field +- Changing the shape of exported interfaces (removing properties, changing types) +- Renaming exported types (consumers import by name) +- Removing union type members +- Changing generic type parameter constraints + +**How to detect:** + +- Check if `package.json` `exports` field is modified +- Check diff for removed `export` statements +- Check diff for changed interface/type shapes + +### Manifest/Extension System + +**Breaking changes:** + +- Renaming a manifest `alias` value — plugin developers reference aliases by string in conditions, overwrites, and extension registry lookups. Alias renames are not caught by the compiler since they are string-based. A renamed alias silently breaks any plugin that references the old string. +- Removing support for a manifest `type` that plugins use +- Changing manifest `alias` resolution or validation +- Removing or renaming manifest `kind` types +- Changing extension bundle structure + +**How to detect:** + +- **Alias renames**: Compare `alias:` values in manifest files before and after. Changed alias strings are Critical — the old alias should be preserved as a deprecated entry. +- Search for changes to manifest type definitions +- Check for removed or renamed manifest kinds + +### Context API + +**Breaking changes:** + +- Removing context tokens from exports +- Changing the shape of data provided by a context +- Removing context provider/consumer mechanisms + +**How to detect:** + +- Check for removed context token exports +- Check for changes to context provider classes + +### Controllers/Lifecycle + +**Breaking changes:** + +- Changing controller base class inheritance requirements +- Removing controller lifecycle hooks +- Breaking cleanup mechanisms in `disconnectedCallback()` + +### Observable/State + +**Breaking changes:** + +- Removing observable properties from the public API +- Changing observable emission patterns + +### npm Publishing + +**Breaking changes:** + +- Changing version constraints that exclude previously-supported versions +- Adding incompatible peer dependency constraints + +**How to detect:** + +- Check if `package.json` `peerDependencies` or `dependencies` changed +- Verify version ranges are not narrowed + +--- + +## Reporting Breaking Changes + +When a breaking change is detected, report: + +1. **What**: The specific change and which public symbol is affected +2. **Pattern**: Which mitigation pattern should be applied (Pattern 1, 2, or 3 for backend) +3. **Severity**: Critical (no mitigation present) or Important (mitigation present but incorrect) +4. **Fix**: Concrete code suggestion showing the correct pattern + +If no breaking changes are detected, state: "No breaking changes detected." diff --git a/.claude/skills/umb-review/references/coding-preferences.md b/.claude/skills/umb-review/references/coding-preferences.md new file mode 100644 index 000000000000..6ac861aa6659 --- /dev/null +++ b/.claude/skills/umb-review/references/coding-preferences.md @@ -0,0 +1,168 @@ +# Coding Preferences & Review Criteria + +These are the coding preferences and code review standards used by the review skill. They define what the review evaluates against. + +--- + +## Testing + +- **Always create blackbox tests** for new/changed code +- Choose the appropriate test level: + - **Unit tests** for isolated logic + - **Integration tests** for application services/use cases + - **E2E tests** for API endpoints + +### Test Class Naming + +- Test classes must be postfixed with `Tests` (e.g., `OrderServiceTests`) +- One test class per class under test + +### Test Method Naming + +**C# tests**: Use the `Can_`/`Cannot_` pattern with PascalCase underscore-separated words: + +- `Can_Schedule_Publish_Invariant` +- `Cannot_Delete_Non_Existing` +- `Can_Schedule_Publish_Single_Culture` + +Large test classes are split into partial files by method: `ContentServiceTests.Delete.cs`, `ContentServiceTests.Publish.cs`. + +**TypeScript tests**: Use BDD-style `it()` with natural language descriptions: + +- `it('should not allow the returned value to be lower than min')` +- `it('converts string to camelCase')` + +### Unit Tests + +- Optional, but must be blackbox tests so refactoring does not break tests + +### Integration Tests + +- Every use case / application service must have integration tests +- Tests run against real database (containerized or similar) +- Test the full flow from application layer through infrastructure + +### E2E Tests + +- Every API endpoint must have E2E tests +- Test realistic scenarios including error cases + +--- + +## Trade-offs + +When making decisions, prioritize: + +- **Readability** over cleverness +- **Flexibility** over rigidity +- Explain trade-offs when deviating from these defaults + +--- + +## Breaking Changes + +- Communicate breaking changes at the **OpenAPI/openapi.json level** +- Clearly document what changed and the migration path + +--- + +## Documentation + +- **Document all public or exported types** (classes, interfaces, types, methods, properties) +- Keep documentation in sync with code changes +- Add **JS Docs** on all public frontend APIs (classes, methods, properties) +- Focus on "why" and usage, not restating the obvious + +--- + +## Dependencies + +- Use what's available in the codebase, unless there is no good choice +- **Flag new dependencies** for review — new packages should be justified +- Prefer well-maintained, widely-used packages + +--- + +## Error Messages & Logging + +- **User-facing errors**: Clear, friendly, actionable +- **Log messages**: Technical, detailed, with context +- Include correlation IDs and relevant data in logs + +--- + +## Security + +- **Always check for security issues** using OWASP Top 10 as baseline +- Flag potential vulnerabilities immediately +- Suggest secure alternatives when spotting risky patterns +- Apply principle of least privilege + +--- + +## Immutability + +- Prefer **immutability** by default +- Allow internal properties to be mutated, as long as they are not direct references coming from the outside + +--- + +## Nullability + +- **TypeScript / JavaScript** + - Prefer `undefined` for optional/omitted values (e.g., optional parameters, props, and fields) + - Use `null` only when the domain model explicitly encodes "no value" or "not set" (e.g., `string | null` from APIs/DB), and be consistent with existing types + - Avoid mixing `null` and `undefined` for the same concept within the same model or API surface + +- **C#** + - use nullable types (e.g., `string?`, `int?`) where absence is valid + - Prefer domain modeling (value objects, options/results, empty collections) over `null` where appropriate, but respect existing conventions in the codebase + +--- + +## C# Specific + +- use Notification pattern (not C# events), Composer pattern (DI registration), Scoping with `Complete()`, Attempt pattern for operation results. + +--- + +## Architecture + +- Follow **Clean Architecture** principles +- **Fail-fast** principle: detect and report errors as early as possible +- Within the established layered architecture (Core/Infrastructure/Web/API), organize code by feature inside each layer where practical, while preserving dependency direction +- One class per file +- Avoid N+1 queries +- Profile before optimizing non-critical paths + +### Type Hierarchy Consistency + +When parallel model types have inconsistent relationships to a shared base type: + +**TypeScript**: manipulations via `Omit`, `Pick`, intersection overrides, or workarounds like `as unknown as` / double-casts to bridge type mismatches. + +**C#**: hiding base members with `new` to change types, explicit interface implementations to mask mismatches, or downcasting base return types in derived classes. + +- **Do NOT suggest** the PR code should deviate from its base type to match a sibling that already deviates. Copying the deviation spreads the problem. +- **Do flag** the architectural inconsistency: parallel models should share a compatible base contract. The model that manipulates or deviates from the base type is the one that needs attention — not the one that extends it correctly. +- **Frame the suggestion** as: "These related models have inconsistent type hierarchies. `{deviating type}` manipulates the base contract of `{base type}`, which forces shared consumers like `{shared utility}` to require a shape that conforming subtypes can't satisfy." + +--- + +## Code Style + +- Follow standard naming conventions for the language (C# or JS/TS) +- Keep components small and focused on a single responsibility +- Prefer early returns +- Small functions +- No nested ternaries + +--- + +## Severity Levels + +| Severity | Meaning | +|----------|---------| +| **Critical** | Must fix before merge — security vulnerabilities, data loss risks, broken functionality | +| **Important** | Should fix — performance issues, missing tests, architectural violations | +| **Suggestion** | Nice to have — readability, minor refactoring, alternative approaches | diff --git a/.claude/skills/umb-review/references/complexity-assessment.md b/.claude/skills/umb-review/references/complexity-assessment.md new file mode 100644 index 000000000000..14d16623a2ea --- /dev/null +++ b/.claude/skills/umb-review/references/complexity-assessment.md @@ -0,0 +1,33 @@ +# PR Complexity Assessment + +Evaluate whether the PR's scope suggests it should be split. This assessment is **informational only** — it never blocks or shortens the review. + +## Always check: Formatting mixed with logic + +This check applies to every PR regardless of size or scope. + +Run both commands and compare per-file line counts: +```bash +git diff {target}...HEAD --stat +git diff {target}...HEAD --stat --ignore-all-space +``` + +For any file where the whitespace-ignored diff is less than **half** the full diff size (and the full diff is over 50 lines), that file has significant formatting changes mixed with logic. Flag it with a split suggestion: "File(s) {list} contain significant formatting changes mixed with logic. Consider a separate formatting-only commit or PR to keep the functional diff reviewable." + +## Multi-project scope check + +Skip this section entirely if ALL production files reside in a single project directory or if the PR is docs-only, test-only, dependency-bump-only, or rename-only. + +Otherwise, flag any dimension that applies: + +| Dimension | Condition | Suggestion | +|---|---|---| +| **Size** | 30+ files OR 1500+ lines, spanning 2+ projects | "If changes in {projectA} and {projectB} are independently functional, they could be separate PRs." | +| **Layer spread** | 3+ layers touched (Core/Infrastructure/Web/API/Frontend), 10+ files | "Consider splitting by layer — e.g., Core+Infrastructure first, then API/Frontend consumers." | +| **Mixed intent** | 2+ intent categories (new feature, bugfix, refactor, dependency update) with 15+ files or 3+ projects | "Consider extracting the {secondary intent} into a separate PR." | + +Intent categories — detect from diff characteristics, not commit messages: +- **New feature**: new files or new `public`/`export` declarations +- **Bug fix**: small targeted edits, no new files (don't co-flag with new feature) +- **Refactor**: file renames, symbols moved but logic unchanged +- **Dependency update**: changes to `.csproj`, `Directory.Packages.props`, `package.json` diff --git a/.claude/skills/umb-review/references/gh-cli-setup.md b/.claude/skills/umb-review/references/gh-cli-setup.md new file mode 100644 index 000000000000..6d788d550fac --- /dev/null +++ b/.claude/skills/umb-review/references/gh-cli-setup.md @@ -0,0 +1,23 @@ +# GH CLI Setup Instructions + +The GitHub CLI (`gh`) is required for this review skill to detect PR target branches. + +## Installation + +Install via Homebrew: + +``` +brew install gh +``` + +Or see https://cli.github.com/ for other installation methods. + +## Authentication + +After installing, authorize by running this in the terminal (use the `!` prefix in Claude Code): + +``` +! gh auth login +``` + +Follow the prompts to authenticate with your GitHub account. diff --git a/.claude/skills/umb-review/references/impact-analysis.md b/.claude/skills/umb-review/references/impact-analysis.md new file mode 100644 index 000000000000..e40fa03c08ea --- /dev/null +++ b/.claude/skills/umb-review/references/impact-analysis.md @@ -0,0 +1,153 @@ +# Impact Analysis Reference + +This document describes how to perform impact analysis during PR review. The goal is to look beyond the diff to understand how changes affect consumers in other parts of the codebase. + +--- + +## 1. Extract Changed Public Symbols + +Scan the diff output for changes to public API surface: + +### Backend (.NET) + +Look for added, modified, or removed lines containing: + +- `public class`, `public abstract class`, `public sealed class` +- `public interface` +- `public record`, `public struct`, `public enum` +- `public` or `protected` methods, properties, fields +- `public static` members +- Constructor signatures on public types + +### Frontend (TypeScript/Lit) + +Look for changes to: + +- `export class`, `export interface`, `export type`, `export enum` +- `export function`, `export const` +- `@property()` decorated fields on exported components +- `@customElement()` registrations +- Entries in `package.json` `exports` field + +Collect a list of all changed public symbol names (type names, method names, property names). + +--- + +## 2. Search for Consumers + +For each changed public symbol, search the `src/` directory for usages **outside the changed file itself**. + +### Grep Strategy + +Use the Grep tool with these settings: + +``` +pattern: {symbol name} +path: src/ +output_mode: files_with_matches +head_limit: 20 +``` + +Use `head_limit: 20` to avoid overwhelming results — if there are more than 20 consumers, note "20+ consumers found" and list the first 20. + +### What to Search For + +For each changed type/method, search for: + +- **Type references**: class name, interface name (e.g., `IContentService`) +- **Method calls**: method name in context (e.g., `\.GetById\(` for a method rename) +- **Constructor usage**: `new TypeName(` +- **DI registrations**: `.AddSingleton`, `.AddScoped<`, `.AddTransient<` +- **Notification handlers**: if a notification type changed, search for `INotificationHandler` and `INotificationAsyncHandler` +- **Interface implementations**: if an interface changed, search for `: IInterfaceName` or `IInterfaceName,` + +### Excluding the Changed File + +When reporting consumers, exclude files that are part of the PR's changes (they're already being reviewed). The interesting consumers are those **outside** the PR that may be affected. + +--- + +## 3. Check Dependency Flow Direction + +The Umbraco architecture enforces strict unidirectional dependencies: + +``` +Api.Management / Api.Delivery (depend on Api.Common) + ↓ +Api.Common (depends on Web.Common) + ↓ +Web.Common (depends on Infrastructure) + ↓ +Infrastructure (depends on Core) + ↓ +Core (no dependencies) +``` + +### Layer Mapping + +Map each changed file to its architectural layer: + +| Path prefix | Layer | +|---|---| +| `src/Umbraco.Core/` | Core | +| `src/Umbraco.Infrastructure/` | Infrastructure | +| `src/Umbraco.PublishedCache.*` | Infrastructure | +| `src/Umbraco.Examine.Lucene/` | Infrastructure | +| `src/Umbraco.Cms.Persistence.*` | Infrastructure | +| `src/Umbraco.Web.Common/` | Web | +| `src/Umbraco.Web.UI/` | Web (Application) | +| `src/Umbraco.Web.Website/` | Web | +| `src/Umbraco.Cms.Api.Common/` | API | +| `src/Umbraco.Cms.Api.Management/` | API | +| `src/Umbraco.Cms.Api.Delivery/` | API | +| `src/Umbraco.Web.UI.Client/` | Frontend | +| `tests/` | Test | + +### Violation Detection + +Flag if a change introduces: + +- **Core depending on Infrastructure**: Core file importing/referencing Infrastructure types +- **Core depending on Web/API**: Core file importing/referencing Web or API types +- **Infrastructure depending on Web/API**: Infrastructure file importing Web or API types +- **Cross-API dependencies**: Management API depending on Delivery API or vice versa + +### How to Check + +1. For each changed file, identify its layer +2. Read the file's `using` statements (C#) or `import` statements (TS) +3. Check if any imports reference a higher layer +4. Also check if new parameters or return types come from higher layers + +--- + +## 4. Flag Cross-Project Risks + +### High-Risk Patterns + +These changes have high ripple potential: + +- **Interface changes in Core** — all implementations in Infrastructure must be updated +- **Notification type changes** — all handlers across the codebase are affected +- **Base class changes** — all derived classes are affected +- **Composer changes** — can affect DI container and runtime behavior globally +- **Shared model/DTO changes** — can affect serialization, API contracts, and consumers + +### What to Report + +For each cross-project risk found, report: + +1. **What changed**: The specific symbol and how it changed +2. **Who is affected**: List of consuming files/projects found via Grep +3. **Risk level**: Whether the consumers will break (compile error), behave differently (runtime), or are unaffected +4. **Recommendation**: Whether the PR should include updates to affected consumers + +--- + +## 5. Performance Notes + +- Use `head_limit: 20` on all Grep searches to cap results +- Only search for symbols that actually changed (not every symbol in the file) +- For very common type names (e.g., `IScope`, `ILogger`), consider adding more context to the search pattern to reduce false positives +- Skip impact analysis for test files — they don't have external consumers +- Skip impact analysis for private/internal members — they can't have external consumers diff --git a/CLAUDE.md b/CLAUDE.md index 05ee07ed7dae..33d837f6a60d 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -423,64 +423,19 @@ APIs use `Asp.Versioning.Mvc`: - Delivery API: `/umbraco/delivery/api/v{version}/*` - OpenAPI/Swagger docs per version -### Backoffice npm Package Structure +### Updating `OpenApi.json` (Management API) -The backoffice (`Umbraco.Web.UI.Client`) is published to npm as **`@umbraco-cms/backoffice`** with a plugin architecture: +When a PR changes Management API controllers or models, the `OpenApi.json` file in the Management API project must be updated: -#### Architecture Overview +1. Run the Umbraco instance locally +2. Open Swagger UI and navigate to the swagger.json link (e.g. `https://localhost:44339/umbraco/swagger/management/swagger.json`) +3. Copy the full JSON content and paste it into `src/Umbraco.Cms.Api.Management/OpenApi.json` -- **Multi-workspace structure**: Subprojects in `src/libs/*`, `src/packages/*`, `src/external/*` -- **Export model**: All exports defined in root `package.json` → `./exports` field -- **Importmap-driven runtime**: Dependencies provided at runtime via importmap (single source of truth) -- **Build-time types**: TypeScript types come from npm peerDependencies -- **Plugin model**: Developers create plugins that import from `@umbraco-cms/backoffice/*` exports +**Important**: Commit only the substantive changes — not IDE-applied formatting (whitespace, reordering, etc.). Extraneous formatting diffs make PRs harder to review and merge-ups more error-prone. -#### Dependency Hoisting Strategy +### Backoffice npm Package -When building for npm (`npm pack`), the `cleanse-pkg.js` script hoists subproject dependencies to root `peerDependencies` with intelligent version range conversion: - -**Version Range Logic** (uses `semver` package): - -1. **Pre-release (0.x.y)**: Convert to explicit range - - Input: `^0.85.0` or `0.85.0` - - Output: `>=0.85.0 <1.0.0` - - Rationale: Pre-release caret only allows patch updates, explicit range allows minor upgrades within 0.x.x - - Example: Plugin can use `@hey-api/openapi-ts@0.91.1` while backoffice uses `0.85.0` - -2. **Stable with caret (^X.Y.Z where X ≥ 1)**: Keep as-is - - Input: `^3.3.1` - - Output: `^3.3.1` (unchanged) - - Rationale: Caret already implements correct semantics for stable versions - -3. **Stable exact versions (X.Y.Z where X ≥ 1)**: Add caret - - Input: `3.16.0` - - Output: `^3.16.0` - - Rationale: Normalizes to conventional semver format - -#### Key Dependencies - -**Runtime via importmap** (types available from peerDependencies): -- `lit`, `rxjs`, `@umbraco-ui/uui` - Core framework -- `monaco-editor`, `@tiptap/*` - Feature-specific editors -- `@hey-api/openapi-ts` - HTTP client type generation - -**Build-time only** (not hoisted): -- `vite`, `typescript`, `eslint` - Dev tooling - -#### Plugin Development Implications - -Plugin developers should: -- **Declare explicit dependencies** in their own `package.json` (avoid relying on transitive deps) -- **Understand the version ranges**: `>=0.85.0 <1.0.0` means they can use newer pre-release versions -- **Know that types match npm ranges**, but runtime comes from importmap (managed by backoffice) -- **When `@hey-api` hits 1.0.0**: Published constraint will automatically become `^1.0.0` - -#### Implementation Details - -- Script location: `src/Umbraco.Web.UI.Client/devops/publish/cleanse-pkg.js` -- Runs as `prepack` hook before npm pack -- Uses `semver.minVersion()` for robust version range parsing -- Generates single source of truth for importmap versions +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". ### Known Limitations @@ -520,6 +475,7 @@ The `Tests:Database:DatabaseType` setting controls which database is used: - `"LocalDb"` - Uses SQL Server LocalDB, required for SQL Server-specific tests (e.g., page-level locking, `sys.dm_tran_locks`) SQL Server-specific tests use `BaseTestDatabase.IsSqlite()` to skip when running on SQLite. + ### Key Projects | Project | Type | Description | diff --git a/Directory.Packages.props b/Directory.Packages.props index 4e9195932bf6..b84a150e8b97 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -14,7 +14,7 @@ - + diff --git a/build/azure-pipelines.yml b/build/azure-pipelines.yml index 338da39ae7be..7d2661c5df38 100644 --- a/build/azure-pipelines.yml +++ b/build/azure-pipelines.yml @@ -117,7 +117,7 @@ stages: artifactName: csharp-docs-dlls - powershell: | dotnet tool install --global CycloneDX - dotnet-CycloneDX $(solution) --output $(Build.ArtifactStagingDirectory)/bom --filename bom-dotnet.xml + dotnet-CycloneDX $(solution) --spec-version 1.5 --output $(Build.ArtifactStagingDirectory)/bom --filename bom-dotnet.xml displayName: 'Generate Backend BOM' - powershell: | npm install --global @cyclonedx/cyclonedx-npm @@ -614,7 +614,6 @@ stages: UMBRACO__CMS__GLOBAL__VERSIONCHECKPERIOD: 0 UMBRACO__CMS__GLOBAL__USEHTTPS: true UMBRACO__CMS__HEALTHCHECKS__NOTIFICATION__ENABLED: false - UMBRACO__CMS__KEEPALIVE__DISABLEKEEPALIVETASK: true UMBRACO__CMS__WEBROUTING__UMBRACOAPPLICATIONURL: https://localhost:44331/ ASPNETCORE_URLS: https://localhost:44331 jobs: @@ -867,6 +866,40 @@ stages: npm publish "${files[0]}" displayName: Push to npm (MyGet) workingDirectory: $(Pipeline.Workspace)/npm + - job: PublishTestHelpersNpm + displayName: Push TestHelpers to pre-release feed (npm) + steps: + - 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" + - 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 @@ -927,7 +960,7 @@ stages: - checkout: none - download: current artifact: npm-testhelpers - - bash: echo "@umbraco:registry=https://registry.npmjs.org" >> .npmrc + - bash: echo "@umbraco-cms:registry=https://registry.npmjs.org" >> .npmrc workingDirectory: $(Pipeline.Workspace)/npm-testhelpers displayName: Add scoped registry to .npmrc - task: npmAuthenticate@0 diff --git a/build/nightly-E2E-test-pipelines.yml b/build/nightly-E2E-test-pipelines.yml index f12f764b0499..aeb2fd50fe71 100644 --- a/build/nightly-E2E-test-pipelines.yml +++ b/build/nightly-E2E-test-pipelines.yml @@ -339,7 +339,6 @@ stages: UMBRACO__CMS__GLOBAL__VERSIONCHECKPERIOD: 0 UMBRACO__CMS__GLOBAL__USEHTTPS: true UMBRACO__CMS__HEALTHCHECKS__NOTIFICATION__ENABLED: false - UMBRACO__CMS__KEEPALIVE__DISABLEKEEPALIVETASK: true UMBRACO__CMS__WEBROUTING__UMBRACOAPPLICATIONURL: https://localhost:44331/ ASPNETCORE_URLS: https://localhost:44331 jobs: diff --git a/src/Umbraco.Cms.Api.Common/DependencyInjection/HideBackOfficeTokensHandler.cs b/src/Umbraco.Cms.Api.Common/DependencyInjection/HideBackOfficeTokensHandler.cs index bc80e9276e8f..8236a922649f 100644 --- a/src/Umbraco.Cms.Api.Common/DependencyInjection/HideBackOfficeTokensHandler.cs +++ b/src/Umbraco.Cms.Api.Common/DependencyInjection/HideBackOfficeTokensHandler.cs @@ -1,6 +1,8 @@ using System.Diagnostics.CodeAnalysis; +using System.Security.Cryptography; using Microsoft.AspNetCore.DataProtection; using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using OpenIddict.Abstractions; using OpenIddict.Server; @@ -41,6 +43,7 @@ internal sealed class HideBackOfficeTokensHandler private readonly IHttpContextAccessor _httpContextAccessor; private readonly IDataProtectionProvider _dataProtectionProvider; + private readonly ILogger _logger; #pragma warning disable CS0618 // Type or member is obsolete private readonly BackOfficeTokenCookieSettings _backOfficeTokenCookieSettings; #pragma warning restore CS0618 // Type or member is obsolete @@ -51,11 +54,13 @@ internal sealed class HideBackOfficeTokensHandler /// /// The HTTP context accessor. /// The data protection provider for encrypting cookie values. + /// The logger. /// The back-office token cookie settings. /// The global settings. public HideBackOfficeTokensHandler( IHttpContextAccessor httpContextAccessor, IDataProtectionProvider dataProtectionProvider, + ILogger logger, #pragma warning disable CS0618 // Type or member is obsolete IOptions backOfficeTokenCookieSettings, #pragma warning restore CS0618 // Type or member is obsolete @@ -63,6 +68,7 @@ public HideBackOfficeTokensHandler( { _httpContextAccessor = httpContextAccessor; _dataProtectionProvider = dataProtectionProvider; + _logger = logger; _backOfficeTokenCookieSettings = backOfficeTokenCookieSettings.Value; _globalSettings = globalSettings.Value; @@ -291,8 +297,19 @@ private bool TryGetCookie(HttpContext httpContext, string cookieName, [NotNullWh var key = GetCookieKey(httpContext, cookieName); if (httpContext.Request.Cookies.TryGetValue(key, out var cookieValue)) { - value = EncryptionHelper.Decrypt(cookieValue, _dataProtectionProvider); - return true; + try + { + value = EncryptionHelper.Decrypt(cookieValue, _dataProtectionProvider); + return true; + } + catch (CryptographicException ex) + { + // Decryption can fail if the data protection key ring has changed + // (e.g., after deployment, app pool recycle, or slot swap). + // Treat this as a missing cookie — the user will need to re-authenticate. + _logger.LogWarning(ex, "Failed to decrypt back-office token cookie '{CookieName}'. The user will need to re-authenticate.", cookieName); + RemoveCookie(httpContext, cookieName); + } } value = null; diff --git a/src/Umbraco.Cms.Api.Delivery/Indexing/Selectors/AncestorsSelectorIndexer.cs b/src/Umbraco.Cms.Api.Delivery/Indexing/Selectors/AncestorsSelectorIndexer.cs index e70faeab67a6..f0624b0ca137 100644 --- a/src/Umbraco.Cms.Api.Delivery/Indexing/Selectors/AncestorsSelectorIndexer.cs +++ b/src/Umbraco.Cms.Api.Delivery/Indexing/Selectors/AncestorsSelectorIndexer.cs @@ -1,12 +1,13 @@ using Umbraco.Cms.Core.DeliveryApi; using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Infrastructure.Examine; namespace Umbraco.Cms.Api.Delivery.Indexing.Selectors; public sealed class AncestorsSelectorIndexer : IContentIndexHandler { // NOTE: "id" is a reserved field name - internal const string FieldName = "itemId"; + internal const string FieldName = UmbracoExamineFieldNames.DeliveryApiContentIndex.ItemId; public IEnumerable GetFieldValues(IContent content, string? culture) => new[] { new IndexFieldValue { FieldName = FieldName, Values = new object[] { content.Key } } }; diff --git a/src/Umbraco.Cms.Api.Delivery/Services/ApiContentQueryProvider.cs b/src/Umbraco.Cms.Api.Delivery/Services/ApiContentQueryProvider.cs index 07453476f68c..b1253a4244ba 100644 --- a/src/Umbraco.Cms.Api.Delivery/Services/ApiContentQueryProvider.cs +++ b/src/Umbraco.Cms.Api.Delivery/Services/ApiContentQueryProvider.cs @@ -17,7 +17,7 @@ namespace Umbraco.Cms.Api.Delivery.Services; /// internal sealed class ApiContentQueryProvider : IApiContentQueryProvider { - private const string ItemIdFieldName = "itemId"; + private const string ItemIdFieldName = UmbracoExamineFieldNames.DeliveryApiContentIndex.ItemId; private readonly IExamineManager _examineManager; private readonly ILogger _logger; private readonly ApiContentQuerySelectorBuilder _selectorBuilder; diff --git a/src/Umbraco.Cms.Api.Management/Controllers/DataType/Tree/DataTypeTreeControllerBase.cs b/src/Umbraco.Cms.Api.Management/Controllers/DataType/Tree/DataTypeTreeControllerBase.cs index b28b5e0a306a..be4f1d00eae9 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/DataType/Tree/DataTypeTreeControllerBase.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/DataType/Tree/DataTypeTreeControllerBase.cs @@ -60,17 +60,17 @@ protected override Ordering ItemOrdering } } - protected override DataTypeTreeItemResponseModel[] MapTreeItemViewModels(Guid? parentId, IEntitySlim[] entities) + protected override async Task MapTreeItemViewModelsAsync(Guid? parentId, IEntitySlim[] entities) { Dictionary dataTypes = entities.Any() - ? _dataTypeService - .GetAllAsync(entities.Select(entity => entity.Key).ToArray()).GetAwaiter().GetResult() + ? (await _dataTypeService + .GetAllAsync(entities.Select(entity => entity.Key).ToArray())) .ToDictionary(contentType => contentType.Id) : new Dictionary(); - return entities.Select(entity => + IEnumerable> tasks = entities.Select(async entity => { - DataTypeTreeItemResponseModel responseModel = MapTreeItemViewModel(parentId, entity); + DataTypeTreeItemResponseModel responseModel = await MapTreeItemViewModelAsync(parentId, entity); if (dataTypes.TryGetValue(entity.Id, out IDataType? dataType)) { responseModel.EditorUiAlias = dataType.EditorUiAlias; @@ -78,6 +78,8 @@ protected override DataTypeTreeItemResponseModel[] MapTreeItemViewModels(Guid? p } return responseModel; - }).ToArray(); + }); + + return await Task.WhenAll(tasks); } } diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Document/Item/ItemDocumentItemController.cs b/src/Umbraco.Cms.Api.Management/Controllers/Document/Item/ItemDocumentItemController.cs index 4d30a12cd1e9..471c0a259def 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/Document/Item/ItemDocumentItemController.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/Document/Item/ItemDocumentItemController.cs @@ -57,7 +57,8 @@ public async Task Item( .GetAll(UmbracoObjectTypes.Document, ids.ToArray()) .OfType(); - IEnumerable responseModels = documents.Select(_documentPresentationFactory.CreateItemResponseModel); + IEnumerable> tasks = documents.Select(_documentPresentationFactory.CreateItemResponseModelAsync); + DocumentItemResponseModel[] responseModels = await Task.WhenAll(tasks); return Ok(responseModels); } } diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Document/Item/SearchDocumentItemController.cs b/src/Umbraco.Cms.Api.Management/Controllers/Document/Item/SearchDocumentItemController.cs index d880e0106c89..554bd8fd11ae 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/Document/Item/SearchDocumentItemController.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/Document/Item/SearchDocumentItemController.cs @@ -78,9 +78,12 @@ public async Task SearchWithTrashed( take, ignoreUserStartNodes); + IEnumerable> tasks = searchResult.Items.OfType().Select(_documentPresentationFactory.CreateItemResponseModelAsync); + DocumentItemResponseModel[] items = await Task.WhenAll(tasks); + var result = new PagedModel { - Items = searchResult.Items.OfType().Select(_documentPresentationFactory.CreateItemResponseModel), + Items = items, Total = searchResult.Total, }; diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Document/RecycleBin/DocumentRecycleBinControllerBase.cs b/src/Umbraco.Cms.Api.Management/Controllers/Document/RecycleBin/DocumentRecycleBinControllerBase.cs index b4560779bc0c..c7e2f5332c8d 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/Document/RecycleBin/DocumentRecycleBinControllerBase.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/Document/RecycleBin/DocumentRecycleBinControllerBase.cs @@ -39,13 +39,13 @@ public DocumentRecycleBinControllerBase(IEntityService entityService, IDocumentP protected override Guid RecycleBinRootKey => Constants.System.RecycleBinContentKey; - protected override DocumentRecycleBinItemResponseModel MapRecycleBinViewModel(Guid? parentId, IEntitySlim entity) + protected override async Task MapRecycleBinViewModelAsync(Guid? parentId, IEntitySlim entity) { - DocumentRecycleBinItemResponseModel responseModel = base.MapRecycleBinViewModel(parentId, entity); + DocumentRecycleBinItemResponseModel responseModel = await base.MapRecycleBinViewModelAsync(parentId, entity); if (entity is IDocumentEntitySlim documentEntitySlim) { - responseModel.Variants = _documentPresentationFactory.CreateVariantsItemResponseModels(documentEntitySlim); + responseModel.Variants = await _documentPresentationFactory.CreateVariantsItemResponseModelsAsync(documentEntitySlim); responseModel.DocumentType = _documentPresentationFactory.CreateDocumentTypeReferenceResponseModel(documentEntitySlim); } diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Document/Tree/DocumentTreeControllerBase.cs b/src/Umbraco.Cms.Api.Management/Controllers/Document/Tree/DocumentTreeControllerBase.cs index 428fe46d90ef..92f505855796 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/Document/Tree/DocumentTreeControllerBase.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/Document/Tree/DocumentTreeControllerBase.cs @@ -81,9 +81,9 @@ protected DocumentTreeControllerBase( protected override Ordering ItemOrdering => Ordering.By(Infrastructure.Persistence.Dtos.NodeDto.SortOrderColumnName); - protected override DocumentTreeItemResponseModel MapTreeItemViewModel(Guid? parentId, IEntitySlim entity) + protected override async Task MapTreeItemViewModelAsync(Guid? parentId, IEntitySlim entity) { - DocumentTreeItemResponseModel responseModel = base.MapTreeItemViewModel(parentId, entity); + DocumentTreeItemResponseModel responseModel = await base.MapTreeItemViewModelAsync(parentId, entity); if (entity is IDocumentEntitySlim documentEntitySlim) { @@ -94,7 +94,7 @@ protected override DocumentTreeItemResponseModel MapTreeItemViewModel(Guid? pare responseModel.Id = entity.Key; responseModel.CreateDate = entity.CreateDate; - responseModel.Variants = _documentPresentationFactory.CreateVariantsItemResponseModels(documentEntitySlim); + responseModel.Variants = await _documentPresentationFactory.CreateVariantsItemResponseModelsAsync(documentEntitySlim); responseModel.DocumentType = _documentPresentationFactory.CreateDocumentTypeReferenceResponseModel(documentEntitySlim); } diff --git a/src/Umbraco.Cms.Api.Management/Controllers/DocumentBlueprint/GetAuditLogDocumentBlueprintController.cs b/src/Umbraco.Cms.Api.Management/Controllers/DocumentBlueprint/GetAuditLogDocumentBlueprintController.cs new file mode 100644 index 000000000000..0a2f6073d4e5 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Controllers/DocumentBlueprint/GetAuditLogDocumentBlueprintController.cs @@ -0,0 +1,47 @@ +using Asp.Versioning; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Api.Common.ViewModels.Pagination; +using Umbraco.Cms.Api.Management.Factories; +using Umbraco.Cms.Api.Management.ViewModels.AuditLog; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Web.Common.Authorization; + +namespace Umbraco.Cms.Api.Management.Controllers.DocumentBlueprint; + +[ApiVersion("1.0")] +[Authorize(Policy = AuthorizationPolicies.TreeAccessDocumentTypes)] +public class GetAuditLogDocumentBlueprintController : DocumentBlueprintControllerBase +{ + private readonly IAuditService _auditService; + private readonly IAuditLogPresentationFactory _auditLogPresentationFactory; + + public GetAuditLogDocumentBlueprintController( + IAuditService auditService, + IAuditLogPresentationFactory auditLogPresentationFactory) + { + _auditService = auditService; + _auditLogPresentationFactory = auditLogPresentationFactory; + } + + [MapToApiVersion("1.0")] + [HttpGet("{id:guid}/audit-log")] + [ProducesResponseType(typeof(PagedViewModel), StatusCodes.Status200OK)] + [EndpointSummary("Gets the audit log for a document blueprint.")] + [EndpointDescription("Gets a paginated collection of audit log entries for the document blueprint identified by the provided Id.")] + public async Task GetAuditLog(CancellationToken cancellationToken, Guid id, Direction orderDirection = Direction.Descending, DateTimeOffset? sinceDate = null, int skip = 0, int take = 100) + { + PagedModel result = await _auditService.GetItemsByKeyAsync(id, UmbracoObjectTypes.DocumentBlueprint, skip, take, orderDirection, sinceDate); + IEnumerable mapped = _auditLogPresentationFactory.CreateAuditLogViewModel(result.Items); + var viewModel = new PagedViewModel + { + Total = result.Total, + Items = mapped, + }; + + return Ok(viewModel); + } +} diff --git a/src/Umbraco.Cms.Api.Management/Controllers/DocumentBlueprint/Tree/DocumentBlueprintTreeControllerBase.cs b/src/Umbraco.Cms.Api.Management/Controllers/DocumentBlueprint/Tree/DocumentBlueprintTreeControllerBase.cs index 4bad663f80dd..29683492e486 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/DocumentBlueprint/Tree/DocumentBlueprintTreeControllerBase.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/DocumentBlueprint/Tree/DocumentBlueprintTreeControllerBase.cs @@ -66,15 +66,19 @@ protected override Ordering ItemOrdering } } - protected override DocumentBlueprintTreeItemResponseModel[] MapTreeItemViewModels(Guid? parentId, IEntitySlim[] entities) - => entities.Select(entity => + protected override async Task MapTreeItemViewModelsAsync(Guid? parentId, IEntitySlim[] entities) + { + IEnumerable> tasks = entities.Select(async entity => { - DocumentBlueprintTreeItemResponseModel responseModel = MapTreeItemViewModel(parentId, entity); + DocumentBlueprintTreeItemResponseModel responseModel = await MapTreeItemViewModelAsync(parentId, entity); if (entity is IDocumentEntitySlim documentEntitySlim) { responseModel.HasChildren = false; responseModel.DocumentType = _documentPresentationFactory.CreateDocumentTypeReferenceResponseModel(documentEntitySlim); } return responseModel; - }).ToArray(); + }); + + return await Task.WhenAll(tasks); + } } diff --git a/src/Umbraco.Cms.Api.Management/Controllers/DocumentType/DocumentTypeControllerBase.cs b/src/Umbraco.Cms.Api.Management/Controllers/DocumentType/DocumentTypeControllerBase.cs index b81adb343927..2a09cc00f771 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/DocumentType/DocumentTypeControllerBase.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/DocumentType/DocumentTypeControllerBase.cs @@ -94,6 +94,10 @@ status is ContentTypeOperationStatus.Success .WithTitle("Operation not permitted") .WithDetail("The attempted operation was not permitted, likely due to a permission/configuration mismatch with the operation.") .Build()), + ContentTypeOperationStatus.SystemAliasChangeNotAllowed => new BadRequestObjectResult(problemDetailsBuilder + .WithTitle("Alias change not permitted") + .WithDetail($"The alias of a system {type} type cannot be changed. To create a {type} type with a different alias, use the duplicate operation instead.") + .Build()), ContentTypeOperationStatus.CancelledByNotification => new BadRequestObjectResult(problemDetailsBuilder .WithTitle("Cancelled by notification") .WithDetail("The attempted operation was cancelled by a notification.") diff --git a/src/Umbraco.Cms.Api.Management/Controllers/DocumentType/Tree/DocumentTypeTreeControllerBase.cs b/src/Umbraco.Cms.Api.Management/Controllers/DocumentType/Tree/DocumentTypeTreeControllerBase.cs index 7b110d84d3e9..9a2126b26e6f 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/DocumentType/Tree/DocumentTypeTreeControllerBase.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/DocumentType/Tree/DocumentTypeTreeControllerBase.cs @@ -55,15 +55,15 @@ public DocumentTypeTreeControllerBase( protected override UmbracoObjectTypes FolderObjectType => UmbracoObjectTypes.DocumentTypeContainer; - protected override DocumentTypeTreeItemResponseModel[] MapTreeItemViewModels(Guid? parentKey, IEntitySlim[] entities) + protected override async Task MapTreeItemViewModelsAsync(Guid? parentKey, IEntitySlim[] entities) { var contentTypes = _contentTypeService .GetMany(entities.Select(entity => entity.Id).ToArray()) .ToDictionary(contentType => contentType.Id); - return entities.Select(entity => + IEnumerable> tasks = entities.Select(async entity => { - DocumentTypeTreeItemResponseModel responseModel = MapTreeItemViewModel(parentKey, entity); + DocumentTypeTreeItemResponseModel responseModel = await MapTreeItemViewModelAsync(parentKey, entity); if (contentTypes.TryGetValue(entity.Id, out IContentType? contentType)) { responseModel.Icon = contentType.Icon ?? responseModel.Icon; @@ -71,6 +71,8 @@ protected override DocumentTypeTreeItemResponseModel[] MapTreeItemViewModels(Gui } return responseModel; - }).ToArray(); + }); + + return await Task.WhenAll(tasks); } } diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Element/Item/ItemElementItemController.cs b/src/Umbraco.Cms.Api.Management/Controllers/Element/Item/ItemElementItemController.cs index f6e53805f905..1e4a86f0bf0e 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/Element/Item/ItemElementItemController.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/Element/Item/ItemElementItemController.cs @@ -42,20 +42,21 @@ public ItemElementItemController( [ProducesResponseType(typeof(IEnumerable), StatusCodes.Status200OK)] [EndpointSummary("Gets a collection of element items.")] [EndpointDescription("Gets a collection of element items identified by the provided Ids.")] - public Task Item( + public async Task Item( CancellationToken cancellationToken, [FromQuery(Name = "id")] HashSet ids) { if (ids.Count is 0) { - return Task.FromResult(Ok(Enumerable.Empty())); + return Ok(Enumerable.Empty()); } IEnumerable elements = _entityService .GetAll(UmbracoObjectTypes.Element, ids.ToArray()) .OfType(); - IEnumerable responseModels = elements.Select(_elementPresentationFactory.CreateItemResponseModel); - return Task.FromResult(Ok(responseModels)); + IEnumerable> tasks = elements.Select(_elementPresentationFactory.CreateItemResponseModelAsync); + ElementItemResponseModel[] responseModels = await Task.WhenAll(tasks); + return Ok(responseModels); } } diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Element/RecycleBin/ElementRecycleBinControllerBase.cs b/src/Umbraco.Cms.Api.Management/Controllers/Element/RecycleBin/ElementRecycleBinControllerBase.cs index b102c901d4c4..36c26281b9fd 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/Element/RecycleBin/ElementRecycleBinControllerBase.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/Element/RecycleBin/ElementRecycleBinControllerBase.cs @@ -47,15 +47,15 @@ public ElementRecycleBinControllerBase( IEntityService entityService, IElementPr protected override Guid RecycleBinRootKey => Constants.System.RecycleBinElementKey; - protected override ElementRecycleBinItemResponseModel MapRecycleBinViewModel(Guid? parentId, IEntitySlim entity) + protected override async Task MapRecycleBinViewModelAsync(Guid? parentId, IEntitySlim entity) { - ElementRecycleBinItemResponseModel responseModel = base.MapRecycleBinViewModel(parentId, entity); + ElementRecycleBinItemResponseModel responseModel = await base.MapRecycleBinViewModelAsync(parentId, entity); responseModel.Name = entity.Name ?? string.Empty; if (entity is IElementEntitySlim elementEntitySlim) { - responseModel.Variants = _elementPresentationFactory.CreateVariantsItemResponseModels(elementEntitySlim); + responseModel.Variants = await _elementPresentationFactory.CreateVariantsItemResponseModelsAsync(elementEntitySlim); responseModel.DocumentType = _elementPresentationFactory.CreateDocumentTypeReferenceResponseModel(elementEntitySlim); responseModel.IsFolder = false; } diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Element/Tree/ChildrenElementTreeController.cs b/src/Umbraco.Cms.Api.Management/Controllers/Element/Tree/ChildrenElementTreeController.cs index b91fec165753..8e8836eb1357 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/Element/Tree/ChildrenElementTreeController.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/Element/Tree/ChildrenElementTreeController.cs @@ -41,15 +41,17 @@ public ChildrenElementTreeController( /// The number of items to skip for pagination. /// The number of items to return for pagination. /// Whether to return only folder items. + /// An optional data type identifier to filter the element items. /// A paginated collection of child element tree items. [HttpGet("children")] [MapToApiVersion("1.0")] [ProducesResponseType(typeof(PagedViewModel), StatusCodes.Status200OK)] [EndpointSummary("Gets a collection of element tree child items.")] [EndpointDescription("Gets a paginated collection of element tree items that are children of the provided parent Id.")] - public async Task>> Children(CancellationToken cancellationToken, Guid parentId, int skip = 0, int take = 100, bool foldersOnly = false) + public async Task>> Children(CancellationToken cancellationToken, Guid parentId, int skip = 0, int take = 100, bool foldersOnly = false, Guid? dataTypeId = null) { RenderFoldersOnly(foldersOnly); + IgnoreUserStartNodesForDataType(dataTypeId); return await GetChildren(parentId, skip, take); } } diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Element/Tree/ElementTreeControllerBase.cs b/src/Umbraco.Cms.Api.Management/Controllers/Element/Tree/ElementTreeControllerBase.cs index 192516e2fb40..f69c32e26af0 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/Element/Tree/ElementTreeControllerBase.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/Element/Tree/ElementTreeControllerBase.cs @@ -77,23 +77,23 @@ protected override string[] GetUserStartNodePaths() .GetElementStartNodePaths(EntityService, _appCaches) ?? []; - protected override ElementTreeItemResponseModel MapTreeItemViewModel(Guid? parentKey, IEntitySlim entity) + protected override async Task MapTreeItemViewModelAsync(Guid? parentKey, IEntitySlim entity) { - ElementTreeItemResponseModel responseModel = base.MapTreeItemViewModel(parentKey, entity); + ElementTreeItemResponseModel responseModel = await base.MapTreeItemViewModelAsync(parentKey, entity); if (entity is IElementEntitySlim elementEntitySlim) { responseModel.CreateDate = elementEntitySlim.CreateDate; responseModel.DocumentType = _elementPresentationFactory.CreateDocumentTypeReferenceResponseModel(elementEntitySlim); - responseModel.Variants = _elementPresentationFactory.CreateVariantsItemResponseModels(elementEntitySlim); + responseModel.Variants = await _elementPresentationFactory.CreateVariantsItemResponseModelsAsync(elementEntitySlim); } return responseModel; } - protected override ElementTreeItemResponseModel MapTreeItemViewModelAsNoAccess(Guid? parentKey, IEntitySlim entity) + protected override async Task MapTreeItemViewModelAsNoAccessAsync(Guid? parentKey, IEntitySlim entity) { - ElementTreeItemResponseModel viewModel = MapTreeItemViewModel(parentKey, entity); + ElementTreeItemResponseModel viewModel = await MapTreeItemViewModelAsync(parentKey, entity); viewModel.NoAccess = true; return viewModel; } diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Element/Tree/RootElementTreeController.cs b/src/Umbraco.Cms.Api.Management/Controllers/Element/Tree/RootElementTreeController.cs index 38cdb3b12834..332822df9408 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/Element/Tree/RootElementTreeController.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/Element/Tree/RootElementTreeController.cs @@ -40,6 +40,7 @@ public RootElementTreeController( /// The number of items to skip for pagination. /// The number of items to return for pagination. /// Whether to return only folder items. + /// An optional identifier to filter element items by data type. /// A paginated collection of root element tree items. [HttpGet("root")] [MapToApiVersion("1.0")] @@ -50,9 +51,11 @@ public async Task>> Ro CancellationToken cancellationToken, int skip = 0, int take = 100, - bool foldersOnly = false) + bool foldersOnly = false, + Guid? dataTypeId = null) { RenderFoldersOnly(foldersOnly); + IgnoreUserStartNodesForDataType(dataTypeId); return await GetRoot(skip, take); } } diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Element/Tree/SiblingsElementTreeController.cs b/src/Umbraco.Cms.Api.Management/Controllers/Element/Tree/SiblingsElementTreeController.cs index 8807c24b6968..f86bd90f3b5d 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/Element/Tree/SiblingsElementTreeController.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/Element/Tree/SiblingsElementTreeController.cs @@ -41,6 +41,7 @@ public SiblingsElementTreeController( /// The number of sibling items to retrieve before the target. /// The number of sibling items to retrieve after the target. /// Whether to return only folder items. + /// An optional data type identifier to filter the sibling items. /// A subset collection of sibling element tree items. [HttpGet("siblings")] [MapToApiVersion("1.0")] @@ -52,9 +53,11 @@ public async Task>> S Guid target, int before, int after, - bool foldersOnly = false) + bool foldersOnly = false, + Guid? dataTypeId = null) { RenderFoldersOnly(foldersOnly); + IgnoreUserStartNodesForDataType(dataTypeId); return await GetSiblings(target, before, after); } } diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Media/RecycleBin/MediaRecycleBinControllerBase.cs b/src/Umbraco.Cms.Api.Management/Controllers/Media/RecycleBin/MediaRecycleBinControllerBase.cs index bd7118eddc60..5c8162961ed1 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/Media/RecycleBin/MediaRecycleBinControllerBase.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/Media/RecycleBin/MediaRecycleBinControllerBase.cs @@ -39,9 +39,9 @@ public MediaRecycleBinControllerBase(IEntityService entityService, IMediaPresent protected override Guid RecycleBinRootKey => Constants.System.RecycleBinMediaKey; - protected override MediaRecycleBinItemResponseModel MapRecycleBinViewModel(Guid? parentKey, IEntitySlim entity) + protected override async Task MapRecycleBinViewModelAsync(Guid? parentKey, IEntitySlim entity) { - MediaRecycleBinItemResponseModel responseModel = base.MapRecycleBinViewModel(parentKey, entity); + MediaRecycleBinItemResponseModel responseModel = await base.MapRecycleBinViewModelAsync(parentKey, entity); if (entity is IMediaEntitySlim mediaEntitySlim) { diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Media/Tree/MediaTreeControllerBase.cs b/src/Umbraco.Cms.Api.Management/Controllers/Media/Tree/MediaTreeControllerBase.cs index 4aa0164c42c4..620640e62a1a 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/Media/Tree/MediaTreeControllerBase.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/Media/Tree/MediaTreeControllerBase.cs @@ -57,9 +57,9 @@ public MediaTreeControllerBase( protected override Ordering ItemOrdering => Ordering.By(Infrastructure.Persistence.Dtos.NodeDto.SortOrderColumnName); - protected override MediaTreeItemResponseModel MapTreeItemViewModel(Guid? parentKey, IEntitySlim entity) + protected override async Task MapTreeItemViewModelAsync(Guid? parentKey, IEntitySlim entity) { - MediaTreeItemResponseModel responseModel = base.MapTreeItemViewModel(parentKey, entity); + MediaTreeItemResponseModel responseModel = await base.MapTreeItemViewModelAsync(parentKey, entity); if (entity is IMediaEntitySlim mediaEntitySlim) { diff --git a/src/Umbraco.Cms.Api.Management/Controllers/MediaType/Tree/MediaTypeTreeControllerBase.cs b/src/Umbraco.Cms.Api.Management/Controllers/MediaType/Tree/MediaTypeTreeControllerBase.cs index e3d7540b5a2e..1d05f657df25 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/MediaType/Tree/MediaTypeTreeControllerBase.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/MediaType/Tree/MediaTypeTreeControllerBase.cs @@ -56,15 +56,15 @@ public MediaTypeTreeControllerBase( protected override UmbracoObjectTypes FolderObjectType => UmbracoObjectTypes.MediaTypeContainer; - protected override MediaTypeTreeItemResponseModel[] MapTreeItemViewModels(Guid? parentKey, IEntitySlim[] entities) + protected override async Task MapTreeItemViewModelsAsync(Guid? parentKey, IEntitySlim[] entities) { var mediaTypes = _mediaTypeService .GetMany(entities.Select(entity => entity.Id).ToArray()) .ToDictionary(contentType => contentType.Id); - return entities.Select(entity => + IEnumerable> tasks = entities.Select(async entity => { - MediaTypeTreeItemResponseModel responseModel = MapTreeItemViewModel(parentKey, entity); + MediaTypeTreeItemResponseModel responseModel = await MapTreeItemViewModelAsync(parentKey, entity); if (mediaTypes.TryGetValue(entity.Id, out IMediaType? mediaType)) { responseModel.Icon = mediaType.Icon ?? responseModel.Icon; @@ -72,6 +72,8 @@ protected override MediaTypeTreeItemResponseModel[] MapTreeItemViewModels(Guid? } return responseModel; - }).ToArray(); + }); + + return await Task.WhenAll(tasks); } } diff --git a/src/Umbraco.Cms.Api.Management/Controllers/MemberGroup/Tree/MemberGroupTreeControllerBase.cs b/src/Umbraco.Cms.Api.Management/Controllers/MemberGroup/Tree/MemberGroupTreeControllerBase.cs index 0fef1b819c39..83f52c0a5d13 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/MemberGroup/Tree/MemberGroupTreeControllerBase.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/MemberGroup/Tree/MemberGroupTreeControllerBase.cs @@ -25,7 +25,7 @@ public class MemberGroupTreeControllerBase : NamedEntityTreeControllerBaseThe service used to manage and retrieve entities within the system. /// A collection of providers that supply additional flags or metadata for entities. public MemberGroupTreeControllerBase( - IEntityService entityService, + IEntityService entityService, FlagProviderCollection flagProviders) : base(entityService, flagProviders) { diff --git a/src/Umbraco.Cms.Api.Management/Controllers/MemberType/Tree/MemberTypeTreeControllerBase.cs b/src/Umbraco.Cms.Api.Management/Controllers/MemberType/Tree/MemberTypeTreeControllerBase.cs index d1010b6c8b0b..f7a8309fddba 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/MemberType/Tree/MemberTypeTreeControllerBase.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/MemberType/Tree/MemberTypeTreeControllerBase.cs @@ -56,21 +56,23 @@ public MemberTypeTreeControllerBase( protected override UmbracoObjectTypes FolderObjectType => UmbracoObjectTypes.MemberTypeContainer; - protected override MemberTypeTreeItemResponseModel[] MapTreeItemViewModels(Guid? parentKey, IEntitySlim[] entities) + protected override async Task MapTreeItemViewModelsAsync(Guid? parentKey, IEntitySlim[] entities) { var memberTypes = _memberTypeService .GetMany(entities.Select(entity => entity.Id).ToArray()) .ToDictionary(contentType => contentType.Id); - return entities.Select(entity => + IEnumerable> tasks = entities.Select(async entity => { - MemberTypeTreeItemResponseModel responseModel = MapTreeItemViewModel(parentKey, entity); + MemberTypeTreeItemResponseModel responseModel = await MapTreeItemViewModelAsync(parentKey, entity); if (memberTypes.TryGetValue(entity.Id, out IMemberType? memberType)) { responseModel.Icon = memberType.Icon ?? responseModel.Icon; } return responseModel; - }).ToArray(); + }); + + return await Task.WhenAll(tasks); } } diff --git a/src/Umbraco.Cms.Api.Management/Controllers/RecycleBin/RecycleBinControllerBase.cs b/src/Umbraco.Cms.Api.Management/Controllers/RecycleBin/RecycleBinControllerBase.cs index 680b4bd661b3..1b3c38b7cd09 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/RecycleBin/RecycleBinControllerBase.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/RecycleBin/RecycleBinControllerBase.cs @@ -28,26 +28,26 @@ protected RecycleBinControllerBase(IEntityService entityService) protected abstract Guid RecycleBinRootKey { get; } - protected Task>> GetRoot(int skip, int take) + protected async Task>> GetRoot(int skip, int take) { IEntitySlim[] rootEntities = GetPagedRootEntities(skip, take, out var totalItems); - TItem[] treeItemViewModels = MapRecycleBinViewModels(null, rootEntities); + TItem[] treeItemViewModels = await MapRecycleBinViewModelsAsync(null, rootEntities); PagedViewModel result = PagedViewModel(treeItemViewModels, totalItems); - return Task.FromResult>>(Ok(result)); + return Ok(result); } - protected Task>> GetChildren(Guid parentKey, int skip, int take) + protected async Task>> GetChildren(Guid parentKey, int skip, int take) { IEntitySlim[] children = GetPagedChildEntities(parentKey, skip, take, out var totalItems); - TItem[] treeItemViewModels = MapRecycleBinViewModels(parentKey, children); + TItem[] treeItemViewModels = await MapRecycleBinViewModelsAsync(parentKey, children); PagedViewModel result = PagedViewModel(treeItemViewModels, totalItems); - return Task.FromResult>>(Ok(result)); + return Ok(result); } protected async Task>> GetSiblings(Guid target, int before, int after) @@ -61,14 +61,14 @@ protected async Task>> GetSiblings(Guid targ IEntitySlim entity = siblings.First(); Guid? parentKey = GetParentKey(entity); - TItem[] treeItemViewModels = MapRecycleBinViewModels(parentKey, siblings); + TItem[] treeItemViewModels = await MapRecycleBinViewModelsAsync(parentKey, siblings); SubsetViewModel result = SubsetViewModel(treeItemViewModels, totalBefore, totalAfter); return Ok(result); } - protected virtual TItem MapRecycleBinViewModel(Guid? parentKey, IEntitySlim entity) + protected virtual Task MapRecycleBinViewModelAsync(Guid? parentKey, IEntitySlim entity) { if (entity == null) { @@ -88,7 +88,7 @@ protected virtual TItem MapRecycleBinViewModel(Guid? parentKey, IEntitySlim enti : null }; - return viewModel; + return Task.FromResult(viewModel); } protected IActionResult OperationStatusResult(OperationResult result) => @@ -154,8 +154,11 @@ protected virtual IEntitySlim[] GetPagedChildEntities(Guid parentKey, int skip, return children; } - private TItem[] MapRecycleBinViewModels(Guid? parentKey, IEntitySlim[] entities) - => entities.Select(entity => MapRecycleBinViewModel(parentKey, entity)).ToArray(); + private async Task MapRecycleBinViewModelsAsync(Guid? parentKey, IEntitySlim[] entities) + { + IEnumerable> tasks = entities.Select(entity => MapRecycleBinViewModelAsync(parentKey, entity)); + return await Task.WhenAll(tasks); + } private PagedViewModel PagedViewModel(IEnumerable treeItemViewModels, long totalItems) => new() { Total = totalItems, Items = treeItemViewModels }; diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Security/ResetPasswordController.cs b/src/Umbraco.Cms.Api.Management/Controllers/Security/ResetPasswordController.cs index e281cc1d65fe..0cb407ca9aad 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/Security/ResetPasswordController.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/Security/ResetPasswordController.cs @@ -49,9 +49,13 @@ public async Task RequestPasswordReset(CancellationToken cancella // If this feature is switched off in configuration, the UI will be amended to not make the request to reset password available. // So this is just a server-side secondary check. + // ApplicationUrlNotConfigured is also surfaced since it is a server-wide configuration issue, not user-specific. // Regardless of other status values, it will just return Ok, so you can't use this endpoint to determine whether the email exists in the system. - return result.Result == UserOperationStatus.CannotPasswordReset - ? BadRequest() - : Ok(); + return result.Result switch + { + UserOperationStatus.CannotPasswordReset => BadRequest(), + UserOperationStatus.ApplicationUrlNotConfigured => UserOperationStatusResult(result.Result), + _ => Ok(), + }; } } diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Security/SecurityControllerBase.cs b/src/Umbraco.Cms.Api.Management/Controllers/Security/SecurityControllerBase.cs index 0e500ae60ad1..f6cea30a91d0 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/Security/SecurityControllerBase.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/Security/SecurityControllerBase.cs @@ -33,6 +33,10 @@ protected IActionResult UserOperationStatusResult(UserOperationStatus status, Er .WithTitle("Unknown failure") .WithDetail(errorMessageResult?.Error?.ErrorMessage ?? "The error was unknown") .Build()), + UserOperationStatus.ApplicationUrlNotConfigured => BadRequest(problemDetailsBuilder + .WithTitle("Application URL not configured") + .WithDetail("The application URL is not configured. Set Umbraco:CMS:WebRouting:UmbracoApplicationUrl in configuration, or change Umbraco:CMS:WebRouting:ApplicationUrlDetection to 'FirstRequest' or 'EveryRequest'.") + .Build()), _ => StatusCode(StatusCodes.Status500InternalServerError, problemDetailsBuilder .WithTitle("Unknown user operation status.") .Build()), diff --git a/src/Umbraco.Cms.Api.Management/Controllers/StaticFile/Tree/StaticFileTreeControllerBase.cs b/src/Umbraco.Cms.Api.Management/Controllers/StaticFile/Tree/StaticFileTreeControllerBase.cs index 5552322d2143..b41d7f29830c 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/StaticFile/Tree/StaticFileTreeControllerBase.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/StaticFile/Tree/StaticFileTreeControllerBase.cs @@ -45,7 +45,7 @@ protected override string[] GetFiles(string path) ? Array.Empty() : _fileSystemTreeService.GetFiles(path); - protected FileSystemTreeItemPresentationModel[] GetAncestorModels(string path, bool includeSelf) + protected override FileSystemTreeItemPresentationModel[] GetAncestorModels(string path, bool includeSelf) => IsAllowedPath(path) ? _fileSystemTreeService.GetAncestorModels(path, includeSelf) : Array.Empty(); diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Template/Tree/TemplateTreeControllerBase.cs b/src/Umbraco.Cms.Api.Management/Controllers/Template/Tree/TemplateTreeControllerBase.cs index 533680bf1c6e..1ee854b36b55 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/Template/Tree/TemplateTreeControllerBase.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/Template/Tree/TemplateTreeControllerBase.cs @@ -26,7 +26,7 @@ public class TemplateTreeControllerBase : NamedEntityTreeControllerBaseThe used for entity operations within the template tree controller. /// A collection of used to provide additional flags or metadata for entities. public TemplateTreeControllerBase( - IEntityService entityService, + IEntityService entityService, FlagProviderCollection flagProviders) : base(entityService, flagProviders) { diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Tree/EntityTreeControllerBase.cs b/src/Umbraco.Cms.Api.Management/Controllers/Tree/EntityTreeControllerBase.cs index 00fa68df4f02..184e2a34f7a0 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/Tree/EntityTreeControllerBase.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/Tree/EntityTreeControllerBase.cs @@ -41,7 +41,7 @@ protected async Task>> GetRoot(int skip, int (rootEntities, totalItems) = await FilterTreeEntities(rootEntities, totalItems); - TItem[] treeItemViewModels = MapTreeItemViewModels((Guid?)null, rootEntities); + TItem[] treeItemViewModels = await MapTreeItemViewModelsAsync((Guid?)null, rootEntities); await PopulateFlags(treeItemViewModels); @@ -56,7 +56,7 @@ protected async Task>> GetChildren(Guid paren (children, totalItems) = await FilterTreeEntities(children, totalItems); - TItem[] treeItemViewModels = MapTreeItemViewModels(parentId, children); + TItem[] treeItemViewModels = await MapTreeItemViewModelsAsync(parentId, children); await PopulateFlags(treeItemViewModels); @@ -78,7 +78,7 @@ protected async Task>> GetSiblings(Guid targ IEntitySlim? entity = siblings.FirstOrDefault(); Guid? parentKey = GetParentKey(entity); - TItem[] treeItemViewModels = MapTreeItemViewModels(parentKey, siblings); + TItem[] treeItemViewModels = await MapTreeItemViewModelsAsync(parentKey, siblings); await PopulateFlags(treeItemViewModels); @@ -137,7 +137,7 @@ protected virtual async Task>> GetAncestors(Guid // All ancestors should be present in the collection, but we defensively use // SingleOrDefault to handle potential data inconsistencies (e.g. after upgrades). List? missingParentIds = null; - TItem[] treeItemViewModels = ancestorEntities + IEnumerable> tasks = ancestorEntities .Select(ancestor => { IEntitySlim? parent = ancestor.ParentId > 0 @@ -150,9 +150,9 @@ protected virtual async Task>> GetAncestors(Guid missingParentIds.Add(ancestor.ParentId); } - return MapTreeItemViewModel(parent?.Key, ancestor); - }) - .ToArray(); + return MapTreeItemViewModelAsync(parent?.Key, ancestor); + }); + TItem[] treeItemViewModels = await Task.WhenAll(tasks); if (missingParentIds is not null) { @@ -230,8 +230,11 @@ protected virtual IEntitySlim[] GetSiblingEntities( ordering: ItemOrdering) .ToArray(); - protected virtual TItem[] MapTreeItemViewModels(Guid? parentKey, IEntitySlim[] entities) - => entities.Select(entity => MapTreeItemViewModel(parentKey, entity)).ToArray(); + protected virtual async Task MapTreeItemViewModelsAsync(Guid? parentKey, IEntitySlim[] entities) + { + IEnumerable> tasks = entities.Select(entity => MapTreeItemViewModelAsync(parentKey, entity)); + return await Task.WhenAll(tasks); + } protected virtual async Task PopulateFlags(TItem[] treeItemViewModels) { @@ -241,7 +244,7 @@ protected virtual async Task PopulateFlags(TItem[] treeItemViewModels) } } - protected virtual TItem MapTreeItemViewModel(Guid? parentKey, IEntitySlim entity) + protected virtual Task MapTreeItemViewModelAsync(Guid? parentKey, IEntitySlim entity) { var viewModel = new TItem { @@ -252,7 +255,7 @@ protected virtual TItem MapTreeItemViewModel(Guid? parentKey, IEntitySlim entity : null, }; - return viewModel; + return Task.FromResult(viewModel); } protected PagedViewModel PagedViewModel(IEnumerable treeItemViewModels, long totalItems) diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Tree/FolderTreeControllerBase.cs b/src/Umbraco.Cms.Api.Management/Controllers/Tree/FolderTreeControllerBase.cs index 03e40a46e446..0ef244a9ac93 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/Tree/FolderTreeControllerBase.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/Tree/FolderTreeControllerBase.cs @@ -122,9 +122,9 @@ protected override IEntitySlim[] GetSiblingEntities(Guid target, int before, int .ToArray(); } - protected override TItem MapTreeItemViewModel(Guid? parentKey, IEntitySlim entity) + protected override async Task MapTreeItemViewModelAsync(Guid? parentKey, IEntitySlim entity) { - TItem viewModel = base.MapTreeItemViewModel(parentKey, entity); + TItem viewModel = await base.MapTreeItemViewModelAsync(parentKey, entity); if (entity.NodeObjectType == _folderObjectTypeId) { @@ -194,7 +194,7 @@ protected async Task>> SearchTreeEntities( (IEntitySlim[] entities, long totalItems) = await FilterTreeEntities(itemSearchResult.Items.ToArray(), itemSearchResult.Total); - TItem[] treeItemViewModels = MapSearchTreeItemViewModels(entities); + TItem[] treeItemViewModels = await MapSearchTreeItemViewModelsAsync(entities); await PopulateFlags(treeItemViewModels); @@ -203,8 +203,11 @@ protected async Task>> SearchTreeEntities( return Ok(result); } - protected virtual TItem[] MapSearchTreeItemViewModels(IEntitySlim[] entities) - => entities.Select(entity => MapTreeItemViewModel(GetSearchResultParentKey(entity), entity)).ToArray(); + protected virtual async Task MapSearchTreeItemViewModelsAsync(IEntitySlim[] entities) + { + IEnumerable> tasks = entities.Select(entity => MapTreeItemViewModelAsync(GetSearchResultParentKey(entity), entity)); + return await Task.WhenAll(tasks); + } private Guid? GetSearchResultParentKey(IEntitySlim entity) { diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Tree/NamedEntityTreeControllerBase.cs b/src/Umbraco.Cms.Api.Management/Controllers/Tree/NamedEntityTreeControllerBase.cs index 3da3643d28f5..6fb28e3e2291 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/Tree/NamedEntityTreeControllerBase.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/Tree/NamedEntityTreeControllerBase.cs @@ -19,9 +19,9 @@ protected NamedEntityTreeControllerBase(IEntityService entityService, FlagProvid { } - protected override TItem MapTreeItemViewModel(Guid? parentKey, IEntitySlim entity) + protected override async Task MapTreeItemViewModelAsync(Guid? parentKey, IEntitySlim entity) { - TItem item = base.MapTreeItemViewModel(parentKey, entity); + TItem item = await base.MapTreeItemViewModelAsync(parentKey, entity); item.Name = entity.Name ?? string.Empty; return item; } diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Tree/UserStartNodeFolderTreeControllerBase.cs b/src/Umbraco.Cms.Api.Management/Controllers/Tree/UserStartNodeFolderTreeControllerBase.cs index 4075b8ad9713..87d69608e39a 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/Tree/UserStartNodeFolderTreeControllerBase.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/Tree/UserStartNodeFolderTreeControllerBase.cs @@ -115,17 +115,17 @@ protected override IEntitySlim[] GetSiblingEntities(Guid target, int before, int } /// - protected override TItem[] MapTreeItemViewModels(Guid? parentKey, IEntitySlim[] entities) + protected override async Task MapTreeItemViewModelsAsync(Guid? parentKey, IEntitySlim[] entities) { if (UserHasRootAccess() || IgnoreUserStartNodes()) { - return base.MapTreeItemViewModels(parentKey, entities); + return await base.MapTreeItemViewModelsAsync(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[] treeItemViewModels = entities.Select(entity => + IEnumerable> tasks = entities.Select(async entity => { if (_accessMap.TryGetValue(entity.Key, out var hasAccess) is false) { @@ -136,9 +136,10 @@ protected override TItem[] MapTreeItemViewModels(Guid? parentKey, IEntitySlim[] // direct access => return a regular item // no direct access => return a "no access" item return hasAccess - ? MapTreeItemViewModel(parentKey, entity) - : MapTreeItemViewModelAsNoAccess(parentKey, entity); - }) + ? await MapTreeItemViewModelAsync(parentKey, entity) + : await MapTreeItemViewModelAsNoAccessAsync(parentKey, entity); + }); + TItem[] treeItemViewModels = (await Task.WhenAll(tasks)) .WhereNotNull() .ToArray(); @@ -154,8 +155,8 @@ protected override TItem[] MapTreeItemViewModels(Guid? parentKey, IEntitySlim[] /// /// Subclasses should override this to set the appropriate "no access" flag on the view model. /// - protected virtual TItem MapTreeItemViewModelAsNoAccess(Guid? parentKey, IEntitySlim entity) - => MapTreeItemViewModel(parentKey, entity); + protected virtual Task MapTreeItemViewModelAsNoAccessAsync(Guid? parentKey, IEntitySlim entity) + => MapTreeItemViewModelAsync(parentKey, entity); private int[] UserStartNodeIds => field ??= GetUserStartNodeIds(); diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Tree/UserStartNodeTreeControllerBase.cs b/src/Umbraco.Cms.Api.Management/Controllers/Tree/UserStartNodeTreeControllerBase.cs index 4b6f83b495b9..bd5dd57fa751 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/Tree/UserStartNodeTreeControllerBase.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/Tree/UserStartNodeTreeControllerBase.cs @@ -86,17 +86,17 @@ protected override IEntitySlim[] GetSiblingEntities(Guid target, int before, int return CalculateAccessMap(() => userAccessEntities, out _); } - protected override TItem[] MapTreeItemViewModels(Guid? parentKey, IEntitySlim[] entities) + protected override async Task MapTreeItemViewModelsAsync(Guid? parentKey, IEntitySlim[] entities) { if (UserHasRootAccess() || IgnoreUserStartNodes()) { - return base.MapTreeItemViewModels(parentKey, entities); + return await base.MapTreeItemViewModelsAsync(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 => + IEnumerable> tasks = entities.Select(async entity => { if (_accessMap.TryGetValue(entity.Key, out var hasAccess) == false) { @@ -107,9 +107,10 @@ protected override TItem[] MapTreeItemViewModels(Guid? parentKey, IEntitySlim[] // direct access => return a regular item // no direct access => return a "no access" item return hasAccess - ? MapTreeItemViewModel(parentKey, entity) - : MapTreeItemViewModelAsNoAccess(parentKey, entity); - }) + ? await MapTreeItemViewModelAsync(parentKey, entity) + : await MapTreeItemViewModelAsNoAccessAsync(parentKey, entity); + }); + TItem[] contentTreeItemViewModels = (await Task.WhenAll(tasks)) .WhereNotNull() .ToArray(); @@ -138,9 +139,9 @@ private IEntitySlim[] CalculateAccessMap(Func> get return entities; } - private TItem MapTreeItemViewModelAsNoAccess(Guid? parentKey, IEntitySlim entity) + private async Task MapTreeItemViewModelAsNoAccessAsync(Guid? parentKey, IEntitySlim entity) { - TItem viewModel = MapTreeItemViewModel(parentKey, entity); + TItem viewModel = await MapTreeItemViewModelAsync(parentKey, entity); viewModel.NoAccess = true; return viewModel; } diff --git a/src/Umbraco.Cms.Api.Management/Controllers/User/UserOrCurrentUserControllerBase.cs b/src/Umbraco.Cms.Api.Management/Controllers/User/UserOrCurrentUserControllerBase.cs index dc5d40848371..729330c3256b 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/User/UserOrCurrentUserControllerBase.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/User/UserOrCurrentUserControllerBase.cs @@ -95,6 +95,10 @@ protected IActionResult UserOperationStatusResult(UserOperationStatus status, Er .WithTitle("Media Start Node not found") .WithDetail("Some of the provided media start nodes was not found.") .Build()), + UserOperationStatus.ElementStartNodeNotFound => BadRequest(problemDetailsBuilder + .WithTitle("Element Start Node not found") + .WithDetail("Some of the provided element start nodes was not found.") + .Build()), UserOperationStatus.UserNotFound => NotFound(problemDetailsBuilder .WithTitle("The user was not found") .WithDetail("The specified user was not found.") @@ -140,6 +144,10 @@ protected IActionResult UserOperationStatusResult(UserOperationStatus status, Er .WithDetail("The target user type does not support this operation.") .Build()), UserOperationStatus.Forbidden => Forbidden(), + UserOperationStatus.ApplicationUrlNotConfigured => BadRequest(problemDetailsBuilder + .WithTitle("Application URL not configured") + .WithDetail("The application URL is not configured. Set Umbraco:CMS:WebRouting:UmbracoApplicationUrl in configuration, or change Umbraco:CMS:WebRouting:ApplicationUrlDetection to 'FirstRequest' or 'EveryRequest'.") + .Build()), _ => StatusCode(StatusCodes.Status500InternalServerError, problemDetailsBuilder .WithTitle("Unknown user operation status.") .Build()), diff --git a/src/Umbraco.Cms.Api.Management/DependencyInjection/InstallerBuilderExtensions.cs b/src/Umbraco.Cms.Api.Management/DependencyInjection/InstallerBuilderExtensions.cs index 6f846bcfdd0a..1f7694cfb007 100644 --- a/src/Umbraco.Cms.Api.Management/DependencyInjection/InstallerBuilderExtensions.cs +++ b/src/Umbraco.Cms.Api.Management/DependencyInjection/InstallerBuilderExtensions.cs @@ -22,7 +22,7 @@ internal static IUmbracoBuilder AddInstaller(this IUmbracoBuilder builder) { IServiceCollection services = builder.Services; - services.AddTransient(); + services.AddTransient(sp => ActivatorUtilities.CreateInstance(sp)); services.AddTransient(); services.AddTransient(); diff --git a/src/Umbraco.Cms.Api.Management/DependencyInjection/PasswordBuilderExtensions.cs b/src/Umbraco.Cms.Api.Management/DependencyInjection/PasswordBuilderExtensions.cs index 3f1fc28e01c2..f4226b29b18b 100644 --- a/src/Umbraco.Cms.Api.Management/DependencyInjection/PasswordBuilderExtensions.cs +++ b/src/Umbraco.Cms.Api.Management/DependencyInjection/PasswordBuilderExtensions.cs @@ -1,4 +1,4 @@ -using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection; using Umbraco.Cms.Api.Management.Factories; using Umbraco.Cms.Core.DependencyInjection; diff --git a/src/Umbraco.Cms.Api.Management/DependencyInjection/UmbracoBuilder.Collections.cs b/src/Umbraco.Cms.Api.Management/DependencyInjection/UmbracoBuilder.Collections.cs index 89c4a85f0bf4..5184aa2ba481 100644 --- a/src/Umbraco.Cms.Api.Management/DependencyInjection/UmbracoBuilder.Collections.cs +++ b/src/Umbraco.Cms.Api.Management/DependencyInjection/UmbracoBuilder.Collections.cs @@ -11,7 +11,8 @@ public static partial class UmbracoBuilderExtensions internal static void AddCollectionBuilders(this IUmbracoBuilder builder) { builder.FlagProviders() - .Append() + .Append() + .Append() .Append() .Append() .Append(); diff --git a/src/Umbraco.Cms.Api.Management/Factories/DocumentPresentationFactory.cs b/src/Umbraco.Cms.Api.Management/Factories/DocumentPresentationFactory.cs index ae0a80229d64..46e328bca53b 100644 --- a/src/Umbraco.Cms.Api.Management/Factories/DocumentPresentationFactory.cs +++ b/src/Umbraco.Cms.Api.Management/Factories/DocumentPresentationFactory.cs @@ -1,4 +1,3 @@ -using Umbraco.Cms.Api.Management.Mapping.Content; using Umbraco.Cms.Api.Management.Services.Flags; using Umbraco.Cms.Api.Management.ViewModels; using Umbraco.Cms.Api.Management.ViewModels.Document; @@ -12,25 +11,23 @@ using Umbraco.Cms.Core.Models.Entities; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Services.OperationStatus; -using Umbraco.Extensions; namespace Umbraco.Cms.Api.Management.Factories; -internal sealed class DocumentPresentationFactory : IDocumentPresentationFactory +/// +internal sealed class DocumentPresentationFactory + : PublishableContentPresentationFactoryBase, + IDocumentPresentationFactory { - private readonly IUmbracoMapper _umbracoMapper; - private readonly IDocumentUrlFactory _documentUrlFactory; private readonly ITemplateService _templateService; private readonly IPublicAccessService _publicAccessService; private readonly TimeProvider _timeProvider; private readonly IIdKeyMap _idKeyMap; - private readonly FlagProviderCollection _flagProviderCollection; /// /// Initializes a new instance of the class. /// /// The mapper used to map between Umbraco models. - /// Factory for generating URLs for documents. /// Service for managing and retrieving templates. /// Service for handling public access and permissions. /// Provider for obtaining the current time. @@ -38,36 +35,26 @@ internal sealed class DocumentPresentationFactory : IDocumentPresentationFactory /// Collection of providers for document flags. public DocumentPresentationFactory( IUmbracoMapper umbracoMapper, - IDocumentUrlFactory documentUrlFactory, ITemplateService templateService, IPublicAccessService publicAccessService, TimeProvider timeProvider, IIdKeyMap idKeyMap, FlagProviderCollection flagProviderCollection) + : base(umbracoMapper, flagProviderCollection) { - _umbracoMapper = umbracoMapper; - _documentUrlFactory = documentUrlFactory; _templateService = templateService; _publicAccessService = publicAccessService; _timeProvider = timeProvider; _idKeyMap = idKeyMap; - _flagProviderCollection = flagProviderCollection; } - /// - /// Asynchronously creates a from the specified instance. - /// - /// The content item from which to generate the published document response model. - /// - /// A task representing the asynchronous operation. The result contains the generated , - /// including template reference information if available. - /// + /// public async Task CreatePublishedResponseModelAsync(IContent content) { - PublishedDocumentResponseModel responseModel = _umbracoMapper.Map(content)!; + PublishedDocumentResponseModel responseModel = UmbracoMapper.Map(content)!; Guid? templateKey = content.PublishTemplateId.HasValue - ? _templateService.GetAsync(content.PublishTemplateId.Value).Result?.Key + ? (await _templateService.GetAsync(content.PublishTemplateId.Value))?.Key : null; responseModel.Template = templateKey.HasValue @@ -77,20 +64,14 @@ public async Task CreatePublishedResponseModelAs return responseModel; } - /// - /// Asynchronously creates a from the specified and . - /// Maps the content and schedule to the response model, and if the content has an associated template, includes its reference in the result. - /// - /// The content item to map to the response model. - /// The collection of content schedules to include in the response model. - /// A task representing the asynchronous operation. The task result contains the constructed , including template reference if applicable. + /// public async Task CreateResponseModelAsync(IContent content, ContentScheduleCollection schedule) { - DocumentResponseModel responseModel = _umbracoMapper.Map(content)!; - _umbracoMapper.Map(schedule, responseModel); + DocumentResponseModel responseModel = UmbracoMapper.Map(content)!; + UmbracoMapper.Map(schedule, responseModel); Guid? templateKey = content.TemplateId.HasValue - ? _templateService.GetAsync(content.TemplateId.Value).Result?.Key + ? (await _templateService.GetAsync(content.TemplateId.Value))?.Key : null; responseModel.Template = templateKey.HasValue @@ -100,15 +81,13 @@ public async Task CreateResponseModelAsync(IContent conte return responseModel; } - /// - /// Creates a from the specified entity. - /// Populates the response model with the document's key properties, including parent reference, trashed status, protection status, document type, variants, and additional flags. - /// - /// The document entity to create the response model from. - /// - /// A representing the document entity, with populated metadata and references. - /// + /// + [Obsolete("Use CreateItemResponseModelAsync instead. Scheduled for removal in Umbraco 19.")] public DocumentItemResponseModel CreateItemResponseModel(IDocumentEntitySlim entity) + => CreateItemResponseModelAsync(entity).GetAwaiter().GetResult(); + + /// + public async Task CreateItemResponseModelAsync(IDocumentEntitySlim entity) { Attempt parentKeyAttempt = _idKeyMap.GetKeyForId(entity.ParentId, UmbracoObjectTypes.Document); @@ -118,82 +97,30 @@ public DocumentItemResponseModel CreateItemResponseModel(IDocumentEntitySlim ent IsTrashed = entity.Trashed, Parent = parentKeyAttempt.Success ? new ReferenceByIdModel { Id = parentKeyAttempt.Result } : null, HasChildren = entity.HasChildren, + IsProtected = _publicAccessService.IsProtected(entity.Path), + DocumentType = CreateDocumentTypeReferenceResponseModel(entity), + Variants = await CreateVariantsItemResponseModelsAsync(entity), }; - responseModel.IsProtected = _publicAccessService.IsProtected(entity.Path); - - responseModel.DocumentType = _umbracoMapper.Map(entity)!; - - responseModel.Variants = CreateVariantsItemResponseModels(entity); - - PopulateFlagsOnDocuments(responseModel); + await PopulateFlagsAsync(responseModel); return responseModel; } + /// public DocumentBlueprintItemResponseModel CreateBlueprintItemResponseModel(IDocumentEntitySlim entity) { var responseModel = new DocumentBlueprintItemResponseModel { Id = entity.Key, Name = entity.Name ?? string.Empty, + DocumentType = UmbracoMapper.Map(entity)!, }; - responseModel.DocumentType = _umbracoMapper.Map(entity)!; - return responseModel; } - /// - /// Generates a collection of objects representing each variant (culture) of the specified document entity. - /// - /// The document entity for which to generate variant response models. - /// An containing a response model for each variant of the document. - public IEnumerable CreateVariantsItemResponseModels(IDocumentEntitySlim entity) - { - if (entity.Variations.VariesByCulture() is false) - { - var model = new DocumentVariantItemResponseModel() - { - Name = entity.Name ?? string.Empty, - State = DocumentVariantStateHelper.GetState(entity, null), - Culture = null, - }; - - PopulateFlagsOnVariants(model); - yield return model; - yield break; - } - - foreach (KeyValuePair cultureNamePair in entity.CultureNames) - { - var model = new DocumentVariantItemResponseModel() - { - Name = cultureNamePair.Value, - Culture = cultureNamePair.Key, - State = DocumentVariantStateHelper.GetState(entity, cultureNamePair.Key) - }; - - PopulateFlagsOnVariants(model); - yield return model; - } - } - - /// - /// Creates a from the given entity. - /// - /// The document entity to map. - /// A mapped instance. - public DocumentTypeReferenceResponseModel CreateDocumentTypeReferenceResponseModel(IDocumentEntitySlim entity) - => _umbracoMapper.Map(entity)!; - - /// - /// Creates a list of instances from the specified . - /// Validates the publish and unpublish times for each culture's schedule, ensuring they are in the future and that unpublish times are after publish times. - /// Returns an containing the resulting list and a indicating the outcome of the validation. - /// - /// The request model containing culture-specific publish schedules to process. - /// An with a list of and the validation status. + /// public Attempt, ContentPublishingOperationStatus> CreateCulturePublishScheduleModels(PublishDocumentRequestModel requestModel) { var model = new List(); @@ -241,19 +168,10 @@ public Attempt, ContentPublishingOperationStat return Attempt.SucceedWithStatus(ContentPublishingOperationStatus.Success, model); } - private void PopulateFlagsOnDocuments(DocumentItemResponseModel model) - { - foreach (IFlagProvider signProvider in _flagProviderCollection.Where(x => x.CanProvideFlags())) - { - signProvider.PopulateFlagsAsync([model]).GetAwaiter().GetResult(); - } - } - - private void PopulateFlagsOnVariants(DocumentVariantItemResponseModel model) - { - foreach (IFlagProvider signProvider in _flagProviderCollection.Where(x => x.CanProvideFlags())) - { - signProvider.PopulateFlagsAsync([model]).GetAwaiter().GetResult(); - } - } + /// + protected override DocumentVariantItemResponseModel CreateVariantItemResponseModel( + string name, + DocumentVariantState state, + string? culture) + => new() { Name = name, State = state, Culture = culture }; } diff --git a/src/Umbraco.Cms.Api.Management/Factories/ElementPresentationFactory.cs b/src/Umbraco.Cms.Api.Management/Factories/ElementPresentationFactory.cs index 337e3452ade0..66687e7dcd9f 100644 --- a/src/Umbraco.Cms.Api.Management/Factories/ElementPresentationFactory.cs +++ b/src/Umbraco.Cms.Api.Management/Factories/ElementPresentationFactory.cs @@ -1,6 +1,6 @@ -using Umbraco.Cms.Api.Management.Mapping.Content; +using Umbraco.Cms.Api.Management.Services.Flags; using Umbraco.Cms.Api.Management.ViewModels; -using Umbraco.Cms.Api.Management.ViewModels.DocumentType; +using Umbraco.Cms.Api.Management.ViewModels.Document; using Umbraco.Cms.Api.Management.ViewModels.Element; using Umbraco.Cms.Api.Management.ViewModels.Element.Item; using Umbraco.Cms.Core; @@ -8,17 +8,14 @@ using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.Entities; using Umbraco.Cms.Core.Services; -using Umbraco.Extensions; namespace Umbraco.Cms.Api.Management.Factories; -// TODO ELEMENTS: lots of code here was duplicated from DocumentPresentationFactory - abstract and refactor -/// -/// Factory for creating element presentation models from domain models. -/// -public class ElementPresentationFactory : IElementPresentationFactory +/// +internal sealed class ElementPresentationFactory + : PublishableContentPresentationFactoryBase, + IElementPresentationFactory { - private readonly IUmbracoMapper _umbracoMapper; private readonly IIdKeyMap _idKeyMap; /// @@ -26,23 +23,25 @@ public class ElementPresentationFactory : IElementPresentationFactory /// /// The mapper used to map between Umbraco models. /// Service for mapping between IDs and keys. - public ElementPresentationFactory(IUmbracoMapper umbracoMapper, IIdKeyMap idKeyMap) - { - _umbracoMapper = umbracoMapper; + /// Collection of providers for document flags. + public ElementPresentationFactory( + IUmbracoMapper umbracoMapper, + IIdKeyMap idKeyMap, + FlagProviderCollection flagProviderCollection) + : base(umbracoMapper, flagProviderCollection) => _idKeyMap = idKeyMap; - } /// public ElementResponseModel CreateResponseModel(IElement element, ContentScheduleCollection schedule) { - ElementResponseModel responseModel = _umbracoMapper.Map(element)!; - _umbracoMapper.Map(schedule, responseModel); + ElementResponseModel responseModel = UmbracoMapper.Map(element)!; + UmbracoMapper.Map(schedule, responseModel); return responseModel; } /// - public ElementItemResponseModel CreateItemResponseModel(IElementEntitySlim entity) + public async Task CreateItemResponseModelAsync(IElementEntitySlim entity) { Attempt parentKeyAttempt = _idKeyMap.GetKeyForId(entity.ParentId, UmbracoObjectTypes.ElementContainer); @@ -51,45 +50,19 @@ public ElementItemResponseModel CreateItemResponseModel(IElementEntitySlim entit Id = entity.Key, Parent = parentKeyAttempt.Success ? new ReferenceByIdModel { Id = parentKeyAttempt.Result } : null, HasChildren = entity.HasChildren, + DocumentType = CreateDocumentTypeReferenceResponseModel(entity), + Variants = await CreateVariantsItemResponseModelsAsync(entity), }; - responseModel.DocumentType = _umbracoMapper.Map(entity)!; - - responseModel.Variants = CreateVariantsItemResponseModels(entity); + await PopulateFlagsAsync(responseModel); return responseModel; } /// - public IEnumerable CreateVariantsItemResponseModels(IElementEntitySlim entity) - { - if (entity.Variations.VariesByCulture() is false) - { - var model = new ElementVariantItemResponseModel() - { - Name = entity.Name ?? string.Empty, - State = DocumentVariantStateHelper.GetState(entity, null), - Culture = null, - }; - - yield return model; - yield break; - } - - foreach (KeyValuePair cultureNamePair in entity.CultureNames) - { - var model = new ElementVariantItemResponseModel() - { - Name = cultureNamePair.Value, - Culture = cultureNamePair.Key, - State = DocumentVariantStateHelper.GetState(entity, cultureNamePair.Key) - }; - - yield return model; - } - } - - /// - public DocumentTypeReferenceResponseModel CreateDocumentTypeReferenceResponseModel(IElementEntitySlim entity) - => _umbracoMapper.Map(entity)!; + protected override ElementVariantItemResponseModel CreateVariantItemResponseModel( + string name, + DocumentVariantState state, + string? culture) + => new() { Name = name, State = state, Culture = culture }; } diff --git a/src/Umbraco.Cms.Api.Management/Factories/IDocumentPresentationFactory.cs b/src/Umbraco.Cms.Api.Management/Factories/IDocumentPresentationFactory.cs index 1e38dd52e493..afefca421594 100644 --- a/src/Umbraco.Cms.Api.Management/Factories/IDocumentPresentationFactory.cs +++ b/src/Umbraco.Cms.Api.Management/Factories/IDocumentPresentationFactory.cs @@ -31,11 +31,13 @@ public interface IDocumentPresentationFactory /// The schedule collection associated with the content. /// A task that represents the asynchronous operation. The task result contains the document response model. Task CreateResponseModelAsync(IContent content, ContentScheduleCollection schedule); + /// /// Creates a response model for a document item based on the provided entity. /// /// The document entity to create the response model from. /// A representing the document item. + [Obsolete("Use CreateItemResponseModelAsync instead. Scheduled for removal in Umbraco 19.")] DocumentItemResponseModel CreateItemResponseModel(IDocumentEntitySlim entity); /// @@ -50,6 +52,7 @@ public interface IDocumentPresentationFactory /// /// The document entity to create variant response models for. /// An enumerable of representing the document variants. + [Obsolete("Use CreateVariantsItemResponseModelsAsync instead. Scheduled for removal in Umbraco 19.")] IEnumerable CreateVariantsItemResponseModels(IDocumentEntitySlim entity); /// @@ -75,11 +78,11 @@ Attempt, ContentPublishingOperationStatus> Cre { if (cultureAndScheduleRequestModel.Schedule is null) { - model.Add(new CulturePublishScheduleModel - { - Culture = cultureAndScheduleRequestModel.Culture - ?? Constants.System.InvariantCulture - }); + model.Add( + new CulturePublishScheduleModel + { + Culture = cultureAndScheduleRequestModel.Culture ?? Constants.System.InvariantCulture, + }); continue; } @@ -100,17 +103,40 @@ Attempt, ContentPublishingOperationStatus> Cre return Attempt.FailWithStatus(ContentPublishingOperationStatus.UnpublishTimeNeedsToBeAfterPublishTime, model); } - model.Add(new CulturePublishScheduleModel - { - Culture = cultureAndScheduleRequestModel.Culture, - Schedule = new ContentScheduleModel + model.Add( + new CulturePublishScheduleModel { - PublishDate = cultureAndScheduleRequestModel.Schedule.PublishTime, - UnpublishDate = cultureAndScheduleRequestModel.Schedule.UnpublishTime, - }, - }); + Culture = cultureAndScheduleRequestModel.Culture, + Schedule = new ContentScheduleModel + { + PublishDate = cultureAndScheduleRequestModel.Schedule.PublishTime, + UnpublishDate = cultureAndScheduleRequestModel.Schedule.UnpublishTime, + }, + }); } return Attempt.SucceedWithStatus(ContentPublishingOperationStatus.Success, model); } + + /// + /// Asynchronously creates a response model for a document item based on the provided entity. + /// + /// The document entity to create the response model from. + /// A task that represents the asynchronous operation. The task result contains a representing the document item. + // TODO (V19): Remove the default implementation when CreateItemResponseModel is removed. + Task CreateItemResponseModelAsync(IDocumentEntitySlim entity) +#pragma warning disable CS0618 // Type or member is obsolete + => Task.FromResult(CreateItemResponseModel(entity)); +#pragma warning restore CS0618 // Type or member is obsolete + + /// + /// Asynchronously creates a collection of instances representing the variants of the specified document entity. + /// + /// The document entity to create variant response models for. + /// A task that represents the asynchronous operation. The task result contains an enumerable of representing the document variants. + // TODO (V19): Remove the default implementation when CreateVariantsItemResponseModels is removed. + Task> CreateVariantsItemResponseModelsAsync(IDocumentEntitySlim entity) +#pragma warning disable CS0618 // Type or member is obsolete + => Task.FromResult(CreateVariantsItemResponseModels(entity)); +#pragma warning restore CS0618 // Type or member is obsolete } diff --git a/src/Umbraco.Cms.Api.Management/Factories/IElementPresentationFactory.cs b/src/Umbraco.Cms.Api.Management/Factories/IElementPresentationFactory.cs index 13e073682444..b1ad8c5594c7 100644 --- a/src/Umbraco.Cms.Api.Management/Factories/IElementPresentationFactory.cs +++ b/src/Umbraco.Cms.Api.Management/Factories/IElementPresentationFactory.cs @@ -1,4 +1,4 @@ -using Umbraco.Cms.Api.Management.ViewModels.DocumentType; +using Umbraco.Cms.Api.Management.ViewModels.DocumentType; using Umbraco.Cms.Api.Management.ViewModels.Element; using Umbraco.Cms.Api.Management.ViewModels.Element.Item; using Umbraco.Cms.Core.Models; @@ -24,14 +24,14 @@ public interface IElementPresentationFactory /// /// The element entity to create the response model from. /// An representing the element item. - ElementItemResponseModel CreateItemResponseModel(IElementEntitySlim entity); + Task CreateItemResponseModelAsync(IElementEntitySlim entity); /// /// Creates a collection of instances representing the variants of the specified element entity. /// /// The element entity to create variant response models for. /// An enumerable of representing the element variants. - IEnumerable CreateVariantsItemResponseModels(IElementEntitySlim entity); + Task> CreateVariantsItemResponseModelsAsync(IElementEntitySlim entity); /// /// Creates a from the given entity. diff --git a/src/Umbraco.Cms.Api.Management/Factories/IPublicAccessPresentationFactory.cs b/src/Umbraco.Cms.Api.Management/Factories/IPublicAccessPresentationFactory.cs index 04b236320d44..dc390da0a274 100644 --- a/src/Umbraco.Cms.Api.Management/Factories/IPublicAccessPresentationFactory.cs +++ b/src/Umbraco.Cms.Api.Management/Factories/IPublicAccessPresentationFactory.cs @@ -20,11 +20,7 @@ public interface IPublicAccessPresentationFactory /// Determines whether the entry is inherited from an ancestor by comparing the entry's protected node key against /// . /// - // TODO (V18): Remove the default implementation. - Attempt CreatePublicAccessResponseModel(PublicAccessEntry entry, Guid contentKey) -#pragma warning disable CS0618 // Type or member is obsolete - => CreatePublicAccessResponseModel(entry); -#pragma warning restore CS0618 // Type or member is obsolete + Attempt CreatePublicAccessResponseModel(PublicAccessEntry entry, Guid contentKey); /// /// Creates a from a . diff --git a/src/Umbraco.Cms.Api.Management/Factories/IndexPresentationFactory.cs b/src/Umbraco.Cms.Api.Management/Factories/IndexPresentationFactory.cs index 039aa9983cb6..59ad9ed1b93b 100644 --- a/src/Umbraco.Cms.Api.Management/Factories/IndexPresentationFactory.cs +++ b/src/Umbraco.Cms.Api.Management/Factories/IndexPresentationFactory.cs @@ -47,6 +47,7 @@ public IndexResponseModel Create(IIndex index) public async Task CreateAsync(IIndex index) { var isCorrupt = !TryGetSearcherName(index, out var searcherName); + var uniqueKeyFieldName = (index as IUmbracoIndex)?.UniqueKeyFieldName; if (await _indexingRebuilderService.IsRebuildingAsync(index.Name)) { @@ -60,6 +61,7 @@ public async Task CreateAsync(IIndex index) SearcherName = searcherName, DocumentCount = 0, FieldCount = 0, + UniqueKeyFieldName = uniqueKeyFieldName, }; } @@ -107,6 +109,7 @@ public async Task CreateAsync(IIndex index) DocumentCount = documentCount, FieldCount = fieldNameCount, ProviderProperties = properties, + UniqueKeyFieldName = uniqueKeyFieldName, }; return indexerModel; diff --git a/src/Umbraco.Cms.Api.Management/Factories/PasswordConfigurationPresentationFactory.cs b/src/Umbraco.Cms.Api.Management/Factories/PasswordConfigurationPresentationFactory.cs index 0bce53c2fbe4..dd374af428eb 100644 --- a/src/Umbraco.Cms.Api.Management/Factories/PasswordConfigurationPresentationFactory.cs +++ b/src/Umbraco.Cms.Api.Management/Factories/PasswordConfigurationPresentationFactory.cs @@ -1,6 +1,8 @@ +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; using Umbraco.Cms.Api.Management.ViewModels.Security; using Umbraco.Cms.Core.Configuration.Models; +using Umbraco.Cms.Core.DependencyInjection; namespace Umbraco.Cms.Api.Management.Factories; @@ -9,21 +11,39 @@ namespace Umbraco.Cms.Api.Management.Factories; /// public class PasswordConfigurationPresentationFactory : IPasswordConfigurationPresentationFactory { - private readonly UserPasswordConfigurationSettings _userPasswordConfigurationSettings; + private readonly SecuritySettings _securitySettings; + + /// + /// Initializes a new instance of the class. + /// + /// An containing the current for user password configuration. + public PasswordConfigurationPresentationFactory(IOptionsSnapshot securitySettings) + => _securitySettings = securitySettings.Value; /// /// Initializes a new instance of the class. /// /// An containing the current for user password configuration. - public PasswordConfigurationPresentationFactory(IOptionsSnapshot userPasswordConfigurationSettings) => _userPasswordConfigurationSettings = userPasswordConfigurationSettings.Value; + [Obsolete("Use the constructor that accepts IOptionsSnapshot instead. Scheduled for removal in Umbraco 19.")] + public PasswordConfigurationPresentationFactory(IOptionsSnapshot userPasswordConfigurationSettings) + : this(StaticServiceProvider.Instance.GetRequiredService>()) + { + } + + // This is just here to resolve an ambiguous constructor. + [Obsolete("Use the constructor that accepts IOptionsSnapshot instead. Scheduled for removal in Umbraco 19.")] + public PasswordConfigurationPresentationFactory(IOptionsSnapshot securitySettings, IOptionsSnapshot _) + : this(securitySettings) + { + } public PasswordConfigurationResponseModel CreatePasswordConfigurationResponseModel() => new() { - MinimumPasswordLength = _userPasswordConfigurationSettings.RequiredLength, - RequireNonLetterOrDigit = _userPasswordConfigurationSettings.RequireNonLetterOrDigit, - RequireDigit = _userPasswordConfigurationSettings.RequireDigit, - RequireLowercase = _userPasswordConfigurationSettings.RequireLowercase, - RequireUppercase = _userPasswordConfigurationSettings.RequireUppercase, + MinimumPasswordLength = _securitySettings.UserPassword.RequiredLength, + RequireNonLetterOrDigit = _securitySettings.UserPassword.RequireNonLetterOrDigit, + RequireDigit = _securitySettings.UserPassword.RequireDigit, + RequireLowercase = _securitySettings.UserPassword.RequireLowercase, + RequireUppercase = _securitySettings.UserPassword.RequireUppercase, }; } diff --git a/src/Umbraco.Cms.Api.Management/Factories/PublishableContentPresentationFactoryBase.cs b/src/Umbraco.Cms.Api.Management/Factories/PublishableContentPresentationFactoryBase.cs new file mode 100644 index 000000000000..74a77451151a --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Factories/PublishableContentPresentationFactoryBase.cs @@ -0,0 +1,124 @@ +using Umbraco.Cms.Api.Management.Mapping.Content; +using Umbraco.Cms.Api.Management.Services.Flags; +using Umbraco.Cms.Api.Management.ViewModels; +using Umbraco.Cms.Api.Management.ViewModels.Content; +using Umbraco.Cms.Api.Management.ViewModels.Document; +using Umbraco.Cms.Api.Management.ViewModels.DocumentType; +using Umbraco.Cms.Core.Mapping; +using Umbraco.Cms.Core.Models.Entities; +using Umbraco.Extensions; + +namespace Umbraco.Cms.Api.Management.Factories; + +/// +/// Base class for presentation factories that create response models for publishable content entities (documents and elements). +/// +/// The publishable content entity type. +/// The variant item response model type. +internal abstract class PublishableContentPresentationFactoryBase + where TEntity : IPublishableContentEntitySlim + where TVariantItemResponseModel : PublishableVariantItemResponseModelBase +{ + private readonly FlagProviderCollection _flagProviderCollection; + + /// + /// Initializes a new instance of the class. + /// + /// The instance used for mapping between models. + /// The instance used for determining flags to populate on response models. + protected PublishableContentPresentationFactoryBase( + IUmbracoMapper umbracoMapper, + FlagProviderCollection flagProviderCollection) + { + UmbracoMapper = umbracoMapper; + _flagProviderCollection = flagProviderCollection; + } + + /// + /// Gets the instance. + /// + protected IUmbracoMapper UmbracoMapper { get; } + + /// + /// Creates variant item response models for an entity, including one per culture for culture-varying content. + /// + /// The entity to create variant models for. + /// The variant item response models. + [Obsolete("Use CreateVariantsItemResponseModelsAsync instead. Scheduled for removal in Umbraco 19.")] + public IEnumerable CreateVariantsItemResponseModels(TEntity entity) + => CreateVariantsItemResponseModelsAsync(entity).GetAwaiter().GetResult(); + + /// + /// Asynchronously creates variant item response models for an entity, including one per culture for culture-varying content. + /// + /// The entity to create variant models for. + /// The variant item response models. + public async Task> CreateVariantsItemResponseModelsAsync(TEntity entity) + { + var models = new List(); + + if (entity.Variations.VariesByCulture() is false) + { + TVariantItemResponseModel model = CreateVariantItemResponseModel(entity.Name ?? string.Empty, DocumentVariantStateHelper.GetState(entity, null), null); + + await PopulateFlagsAsync(model); + models.Add(model); + return models; + } + + foreach (KeyValuePair cultureNamePair in entity.CultureNames) + { + TVariantItemResponseModel model = CreateVariantItemResponseModel(cultureNamePair.Value, DocumentVariantStateHelper.GetState(entity, cultureNamePair.Key), cultureNamePair.Key); + + await PopulateFlagsAsync(model); + models.Add(model); + } + + return models; + } + + /// + /// Creates a for the given entity. + /// + /// The entity to create the document type reference for. + /// The document type reference response model. + public DocumentTypeReferenceResponseModel CreateDocumentTypeReferenceResponseModel(TEntity entity) + => UmbracoMapper.Map(entity)!; + + /// + /// Creates a new variant item response model instance with the specified values. + /// + /// The variant name. + /// The variant state. + /// The culture, or null for invariant content. + /// A new variant item response model. + protected abstract TVariantItemResponseModel CreateVariantItemResponseModel( + string name, + DocumentVariantState state, + string? culture); + + /// + /// Populates flags on a model using all applicable flag providers. + /// + /// The type of the model supporting flags. + /// The model to populate flags on. + [Obsolete("Use PopulateFlagsAsync instead. Scheduled for removal in Umbraco 19.")] + protected void PopulateFlags(TItem model) + where TItem : IHasFlags + => PopulateFlagsAsync(model).GetAwaiter().GetResult(); + + /// + /// Asynchronously populates flags on a model using all applicable flag providers. + /// + /// The type of the model supporting flags. + /// The model to populate flags on. + /// A representing the asynchronous operation. + protected async Task PopulateFlagsAsync(TItem model) + where TItem : IHasFlags + { + foreach (IFlagProvider flagProvider in _flagProviderCollection.Where(x => x.CanProvideFlags())) + { + await flagProvider.PopulateFlagsAsync([model]); + } + } +} diff --git a/src/Umbraco.Cms.Api.Management/Factories/RelationTypePresentationFactory.cs b/src/Umbraco.Cms.Api.Management/Factories/RelationTypePresentationFactory.cs index 6c7ebb7aa37a..37575cef40a2 100644 --- a/src/Umbraco.Cms.Api.Management/Factories/RelationTypePresentationFactory.cs +++ b/src/Umbraco.Cms.Api.Management/Factories/RelationTypePresentationFactory.cs @@ -65,7 +65,7 @@ public RelationTypePresentationFactory( } /// - public Task> CreateReferenceResponseModelsAsync( + public async Task> CreateReferenceResponseModelsAsync( IEnumerable relationItemModels) { IReadOnlyCollection relationItemModelsCollection = relationItemModels.ToArray(); @@ -81,17 +81,19 @@ public Task> CreateReferenceResponseModelsA Constants.UdiEntityType.Element, Constants.ObjectTypes.Element); - IReferenceResponseModel[] result = relationItemModelsCollection.Select(relationItemModel => + IEnumerable> tasks = relationItemModelsCollection.Select>(async relationItemModel => relationItemModel.NodeType switch { - Constants.ReferenceType.Document => MapReference( + Constants.ReferenceType.Document => await MapReference( relationItemModel, documentSlimEntities, - (r, e) => r.Variants = _documentPresentationFactory.CreateVariantsItemResponseModels(e)), - Constants.ReferenceType.Element => MapReference( + async (r, e) => r.Variants + = await _documentPresentationFactory.CreateVariantsItemResponseModelsAsync(e)), + Constants.ReferenceType.Element => await MapReference( relationItemModel, elementSlimEntities, - (r, e) => r.Variants = _elementPresentationFactory.CreateVariantsItemResponseModels(e)), + async (r, e) => r.Variants + = await _elementPresentationFactory.CreateVariantsItemResponseModelsAsync(e)), Constants.ReferenceType.ElementContainer => _umbracoMapper.Map(relationItemModel), Constants.ReferenceType.Media => _umbracoMapper.Map(relationItemModel), Constants.ReferenceType.Member => _umbracoMapper.Map(relationItemModel), @@ -99,15 +101,16 @@ public Task> CreateReferenceResponseModelsA Constants.ReferenceType.MediaTypePropertyType => _umbracoMapper.Map(relationItemModel), Constants.ReferenceType.MemberTypePropertyType => _umbracoMapper.Map(relationItemModel), _ => _umbracoMapper.Map(relationItemModel), - }).WhereNotNull().ToArray(); + }); + IReferenceResponseModel?[] results = await Task.WhenAll(tasks); - return Task.FromResult>(result); + return results.WhereNotNull().ToArray(); } - private TResponse? MapReference( + private async Task MapReference( RelationItemModel relationItemModel, List slimEntities, - Action enrichResponse) + Func enrichResponse) where TResponse : class where TEntity : class, IEntitySlim { @@ -118,7 +121,7 @@ public Task> CreateReferenceResponseModelsA return responseModel; } - enrichResponse(responseModel, matchingEntity); + await enrichResponse(responseModel, matchingEntity); return responseModel; } diff --git a/src/Umbraco.Cms.Api.Management/Filters/UserPasswordEnsureMinimumResponseTimeAttribute.cs b/src/Umbraco.Cms.Api.Management/Filters/UserPasswordEnsureMinimumResponseTimeAttribute.cs index 34440fc0fdb9..ba557acda9fa 100644 --- a/src/Umbraco.Cms.Api.Management/Filters/UserPasswordEnsureMinimumResponseTimeAttribute.cs +++ b/src/Umbraco.Cms.Api.Management/Filters/UserPasswordEnsureMinimumResponseTimeAttribute.cs @@ -20,8 +20,8 @@ private sealed class UserPasswordEnsureMinimumResponseTimeFilter : EnsureMinimum /// Initializes a new instance of the class with the specified user password configuration settings. /// /// The options containing user password configuration settings. - public UserPasswordEnsureMinimumResponseTimeFilter(IOptions options) - : base(options.Value.MinimumResponseTime) + public UserPasswordEnsureMinimumResponseTimeFilter(IOptions options) + : base(options.Value.UserPassword.MinimumResponseTime) { } } diff --git a/src/Umbraco.Cms.Api.Management/Mapping/Content/DocumentVariantStateHelper.cs b/src/Umbraco.Cms.Api.Management/Mapping/Content/DocumentVariantStateHelper.cs index bf9474ce20f9..6eb353aab9a5 100644 --- a/src/Umbraco.Cms.Api.Management/Mapping/Content/DocumentVariantStateHelper.cs +++ b/src/Umbraco.Cms.Api.Management/Mapping/Content/DocumentVariantStateHelper.cs @@ -18,27 +18,22 @@ internal static DocumentVariantState GetState(IPublishableContentBase content, s content.EditedCultures ?? Enumerable.Empty(), content.PublishedCultures); - internal static DocumentVariantState GetState(IDocumentEntitySlim content, string? culture) + internal static DocumentVariantState GetState(IPublishableContentEntitySlim entity, string? culture) => GetState( - content, + entity, culture, - content.Edited, - content.Published, - content.Trashed, - content.CultureNames.Keys, - content.EditedCultures, - content.PublishedCultures); + entity.Edited, + entity.Published, + entity.Trashed, + entity.CultureNames.Keys, + entity.EditedCultures, + entity.PublishedCultures); + + internal static DocumentVariantState GetState(IDocumentEntitySlim content, string? culture) + => GetState((IPublishableContentEntitySlim)content, culture); internal static DocumentVariantState GetState(IElementEntitySlim element, string? culture) - => GetState( - element, - culture, - element.Edited, - element.Published, - element.Trashed, - element.CultureNames.Keys, - element.EditedCultures, - element.PublishedCultures); + => GetState((IPublishableContentEntitySlim)element, culture); private static DocumentVariantState GetState(IEntity entity, string? culture, bool edited, bool published, bool trashed, IEnumerable availableCultures, IEnumerable editedCultures, IEnumerable publishedCultures) { diff --git a/src/Umbraco.Cms.Api.Management/OpenApi.json b/src/Umbraco.Cms.Api.Management/OpenApi.json index 17bec416c5e9..e73d5aeb598b 100644 --- a/src/Umbraco.Cms.Api.Management/OpenApi.json +++ b/src/Umbraco.Cms.Api.Management/OpenApi.json @@ -55,7 +55,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -203,7 +203,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -265,7 +265,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] }, @@ -376,7 +376,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] }, @@ -518,7 +518,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -651,7 +651,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -709,7 +709,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -827,7 +827,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -893,7 +893,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -953,7 +953,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -1004,7 +1004,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -1041,7 +1041,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -1189,7 +1189,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -1251,7 +1251,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] }, @@ -1362,7 +1362,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] }, @@ -1504,7 +1504,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -1552,7 +1552,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -1631,7 +1631,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -1682,7 +1682,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -1733,7 +1733,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -1794,7 +1794,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -1844,7 +1844,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -1917,7 +1917,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -1982,7 +1982,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -2051,7 +2051,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -2122,7 +2122,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -2186,7 +2186,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] }, @@ -2358,7 +2358,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -2420,7 +2420,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] }, @@ -2531,7 +2531,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] }, @@ -2673,7 +2673,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -2744,7 +2744,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -2888,7 +2888,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -3036,7 +3036,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -3087,7 +3087,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -3137,7 +3137,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -3202,7 +3202,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -3259,7 +3259,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -3407,7 +3407,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -3469,7 +3469,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] }, @@ -3580,7 +3580,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] }, @@ -3722,7 +3722,88 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] + } + ] + } + }, + "/umbraco/management/api/v1/document-blueprint/{id}/audit-log": { + "get": { + "tags": [ + "Document Blueprint" + ], + "summary": "Gets the audit log for a document blueprint.", + "description": "Gets a paginated collection of audit log entries for the document blueprint identified by the provided Id.", + "operationId": "GetDocumentBlueprintByIdAuditLog", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "orderDirection", + "in": "query", + "schema": { + "$ref": "#/components/schemas/DirectionModel" + } + }, + { + "name": "sinceDate", + "in": "query", + "schema": { + "type": "string", + "format": "date-time" + } + }, + { + "name": "skip", + "in": "query", + "schema": { + "type": "integer", + "format": "int32", + "default": 0 + } + }, + { + "name": "take", + "in": "query", + "schema": { + "type": "integer", + "format": "int32", + "default": 100 + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/PagedAuditLogResponseModel" + } + ] + } + } + } + }, + "401": { + "description": "The resource is protected and requires an authentication token" + }, + "403": { + "description": "The authenticated user does not have access to this resource" + } + }, + "security": [ + { + "Backoffice-User": [ ] } ] } @@ -3840,7 +3921,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -3902,7 +3983,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -4050,7 +4131,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -4112,7 +4193,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] }, @@ -4223,7 +4304,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] }, @@ -4365,7 +4446,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -4487,7 +4568,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -4538,7 +4619,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -4588,7 +4669,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -4661,7 +4742,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -4726,7 +4807,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -4797,7 +4878,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -4945,7 +5026,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -5007,7 +5088,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] }, @@ -5092,7 +5173,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] }, @@ -5234,7 +5315,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -5322,7 +5403,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -5384,7 +5465,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -5464,7 +5545,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -5543,7 +5624,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -5702,7 +5783,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -5765,7 +5846,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -5909,7 +5990,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -6053,7 +6134,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -6116,7 +6197,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -6249,7 +6330,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -6306,7 +6387,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -6363,7 +6444,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -6458,7 +6539,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -6509,7 +6590,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -6546,7 +6627,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -6694,7 +6775,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -6756,7 +6837,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] }, @@ -6867,7 +6948,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] }, @@ -7009,7 +7090,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -7157,7 +7238,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -7208,7 +7289,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -7259,7 +7340,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -7327,7 +7408,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -7377,7 +7458,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -7450,7 +7531,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -7515,7 +7596,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -7584,7 +7665,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -7655,7 +7736,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -7756,7 +7837,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -7832,7 +7913,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -7952,7 +8033,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -8072,7 +8153,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -8203,7 +8284,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -8351,7 +8432,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -8413,7 +8494,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] }, @@ -8524,7 +8605,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] }, @@ -8666,7 +8747,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -8747,7 +8828,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -8828,7 +8909,7 @@ "deprecated": true, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -8961,7 +9042,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -9023,7 +9104,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] }, @@ -9191,7 +9272,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -9309,7 +9390,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -9422,7 +9503,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -9487,7 +9568,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] }, @@ -9603,7 +9684,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -9700,7 +9781,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -9833,7 +9914,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] }, @@ -9918,7 +9999,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] }, @@ -9986,7 +10067,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] }, @@ -10102,7 +10183,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -10246,7 +10327,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -10401,7 +10482,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -10486,7 +10567,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -10548,7 +10629,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -10628,7 +10709,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -10708,7 +10789,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -10852,7 +10933,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -10996,7 +11077,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -11065,7 +11146,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -11102,7 +11183,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -11235,7 +11316,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -11289,7 +11370,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -11422,7 +11503,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -11473,7 +11554,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -11524,7 +11605,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -11626,7 +11707,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -11702,7 +11783,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -11815,7 +11896,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -11891,7 +11972,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -12035,7 +12116,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -12100,7 +12181,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -12157,7 +12238,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -12214,7 +12295,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -12285,7 +12366,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -12335,7 +12416,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -12408,7 +12489,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -12473,7 +12554,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -12544,7 +12625,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -12636,7 +12717,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -12672,7 +12753,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -12773,7 +12854,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -12849,7 +12930,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -12969,7 +13050,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -13089,7 +13170,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -13237,7 +13318,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -13299,7 +13380,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] }, @@ -13410,7 +13491,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] }, @@ -13552,7 +13633,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -13633,7 +13714,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -13766,7 +13847,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -13884,7 +13965,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -13997,7 +14078,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -14141,7 +14222,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -14221,7 +14302,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -14365,7 +14446,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -14509,7 +14590,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -14578,7 +14659,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -14615,7 +14696,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -14763,7 +14844,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -14825,7 +14906,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] }, @@ -14936,7 +15017,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] }, @@ -15078,7 +15159,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -15222,7 +15303,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -15335,7 +15416,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -15415,7 +15496,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -15548,7 +15629,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -15599,7 +15680,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -15650,7 +15731,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -15726,7 +15807,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -15839,7 +15920,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -15915,7 +15996,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -16059,7 +16140,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -16124,7 +16205,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -16237,7 +16318,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -16313,7 +16394,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -16457,7 +16538,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -16514,7 +16595,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -16571,7 +16652,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -16642,7 +16723,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -16692,7 +16773,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -16739,6 +16820,14 @@ "type": "boolean", "default": false } + }, + { + "name": "dataTypeId", + "in": "query", + "schema": { + "type": "string", + "format": "uuid" + } } ], "responses": { @@ -16765,7 +16854,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -16804,6 +16893,14 @@ "type": "boolean", "default": false } + }, + { + "name": "dataTypeId", + "in": "query", + "schema": { + "type": "string", + "format": "uuid" + } } ], "responses": { @@ -16830,7 +16927,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -16875,6 +16972,14 @@ "type": "boolean", "default": false } + }, + { + "name": "dataTypeId", + "in": "query", + "schema": { + "type": "string", + "format": "uuid" + } } ], "responses": { @@ -16901,7 +17006,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -16958,7 +17063,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -17019,7 +17124,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -17116,7 +17221,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -17234,7 +17339,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -17325,7 +17430,7 @@ "deprecated": true, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -17411,7 +17516,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -17483,7 +17588,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -17537,7 +17642,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -17595,7 +17700,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -17718,7 +17823,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -17976,7 +18081,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -18010,7 +18115,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -18064,7 +18169,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] }, @@ -18210,7 +18315,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -18268,7 +18373,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] }, @@ -18378,7 +18483,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] }, @@ -18519,7 +18624,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -18576,7 +18681,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -18645,7 +18750,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -18742,7 +18847,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -18829,7 +18934,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -18886,7 +18991,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] }, @@ -19006,7 +19111,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -19067,7 +19172,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] }, @@ -19151,7 +19256,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -19209,7 +19314,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -19249,7 +19354,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -19289,7 +19394,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -19369,7 +19474,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -19430,7 +19535,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -19481,7 +19586,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -19535,7 +19640,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -19596,7 +19701,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -19744,7 +19849,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -19806,7 +19911,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] }, @@ -19891,7 +19996,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] }, @@ -20033,7 +20138,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -20121,7 +20226,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -20183,7 +20288,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -20262,7 +20367,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -20421,7 +20526,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -20484,7 +20589,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -20628,7 +20733,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -20772,7 +20877,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -20835,7 +20940,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -20892,7 +20997,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -20987,7 +21092,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -21038,7 +21143,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -21075,7 +21180,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -21223,7 +21328,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -21285,7 +21390,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] }, @@ -21396,7 +21501,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] }, @@ -21538,7 +21643,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -21686,7 +21791,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -21736,7 +21841,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -21809,7 +21914,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -21874,7 +21979,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -21945,7 +22050,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -22068,7 +22173,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -22119,7 +22224,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -22170,7 +22275,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -22272,7 +22377,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -22420,7 +22525,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -22482,7 +22587,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] }, @@ -22593,7 +22698,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] }, @@ -22735,7 +22840,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -22816,7 +22921,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -22934,7 +23039,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -23047,7 +23152,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -23127,7 +23232,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -23207,7 +23312,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -23351,7 +23456,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -23420,7 +23525,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -23457,7 +23562,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -23590,7 +23695,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -23644,7 +23749,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -23777,7 +23882,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -23853,7 +23958,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -23966,7 +24071,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -24042,7 +24147,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -24186,7 +24291,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -24251,7 +24356,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -24308,7 +24413,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -24365,7 +24470,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -24436,7 +24541,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -24486,7 +24591,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -24559,7 +24664,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -24624,7 +24729,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -24695,7 +24800,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -24746,7 +24851,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -24803,7 +24908,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] }, @@ -24923,7 +25028,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -24974,7 +25079,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] }, @@ -25085,7 +25190,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] }, @@ -25227,7 +25332,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -25284,7 +25389,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -25335,7 +25440,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -25386,7 +25491,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -25447,7 +25552,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -25595,7 +25700,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -25657,7 +25762,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] }, @@ -25742,7 +25847,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] }, @@ -25884,7 +25989,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -25963,7 +26068,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -26122,7 +26227,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -26185,7 +26290,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -26329,7 +26434,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -26473,7 +26578,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -26536,7 +26641,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -26593,7 +26698,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -26688,7 +26793,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -26739,7 +26844,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -26776,7 +26881,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -26924,7 +27029,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -26986,7 +27091,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] }, @@ -27097,7 +27202,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] }, @@ -27239,7 +27344,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -27387,7 +27492,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -27437,7 +27542,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -27510,7 +27615,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -27575,7 +27680,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -27646,7 +27751,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -27768,7 +27873,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -27819,7 +27924,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -27870,7 +27975,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -27942,7 +28047,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -28090,7 +28195,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -28152,7 +28257,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] }, @@ -28263,7 +28368,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] }, @@ -28405,7 +28510,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -28485,7 +28590,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -28565,7 +28670,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -28709,7 +28814,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -28778,7 +28883,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -28815,7 +28920,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -28948,7 +29053,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -29024,7 +29129,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -29061,7 +29166,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -29098,7 +29203,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -29132,7 +29237,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -29186,7 +29291,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -29249,7 +29354,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -29361,7 +29466,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -29398,7 +29503,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -29455,7 +29560,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] }, @@ -29601,7 +29706,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -29663,7 +29768,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] }, @@ -29748,7 +29853,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] }, @@ -29864,7 +29969,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -29927,7 +30032,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -29984,7 +30089,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -30034,7 +30139,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -30182,7 +30287,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -30243,7 +30348,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] }, @@ -30353,7 +30458,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] }, @@ -30494,7 +30599,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -30652,7 +30757,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -30800,7 +30905,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -30861,7 +30966,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] }, @@ -30971,7 +31076,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -31028,7 +31133,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -31089,7 +31194,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -31138,7 +31243,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -31202,7 +31307,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -31259,7 +31364,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -31321,7 +31426,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -31385,7 +31490,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] }, @@ -31464,7 +31569,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -31528,7 +31633,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -31563,7 +31668,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -31597,7 +31702,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -31632,7 +31737,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -31710,7 +31815,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -31776,7 +31881,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] }, @@ -31835,7 +31940,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -31872,7 +31977,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] }, @@ -31929,7 +32034,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -31980,7 +32085,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -32037,7 +32142,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -32099,7 +32204,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -32179,7 +32284,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -32229,7 +32334,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -32377,7 +32482,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -32438,7 +32543,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] }, @@ -32548,7 +32653,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] }, @@ -32689,7 +32794,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -32847,7 +32952,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -32995,7 +33100,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -33056,7 +33161,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] }, @@ -33166,7 +33271,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -33215,7 +33320,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -33279,7 +33384,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -33336,7 +33441,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -33398,7 +33503,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -33452,7 +33557,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -33535,7 +33640,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -33572,7 +33677,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -33679,7 +33784,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -33812,7 +33917,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -34001,7 +34106,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -34061,7 +34166,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -34135,7 +34240,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -34173,7 +34278,7 @@ "deprecated": true, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -34223,7 +34328,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -34269,7 +34374,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -34330,7 +34435,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -34384,7 +34489,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -34434,7 +34539,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -34582,7 +34687,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -34643,7 +34748,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] }, @@ -34753,7 +34858,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] }, @@ -34894,7 +34999,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -35052,7 +35157,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -35200,7 +35305,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -35261,7 +35366,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] }, @@ -35371,7 +35476,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -35420,7 +35525,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -35484,7 +35589,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -35541,7 +35646,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -35603,7 +35708,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -35678,7 +35783,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -35735,7 +35840,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -35772,7 +35877,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] }, @@ -35877,7 +35982,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -35928,7 +36033,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -35979,7 +36084,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -36040,7 +36145,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -36188,7 +36293,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -36250,7 +36355,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] }, @@ -36361,7 +36466,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] }, @@ -36503,7 +36608,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -36540,7 +36645,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -36632,7 +36737,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -36669,7 +36774,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -36719,7 +36824,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -36784,7 +36889,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -36841,7 +36946,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -36904,7 +37009,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -37011,7 +37116,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -37084,7 +37189,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] }, @@ -37180,7 +37285,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -37214,7 +37319,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -37316,7 +37421,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -37367,7 +37472,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -37492,7 +37597,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] }, @@ -37564,7 +37669,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] }, @@ -37672,7 +37777,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -37720,7 +37825,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] }, @@ -37808,7 +37913,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -37901,7 +38006,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -37952,7 +38057,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -38059,7 +38164,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] }, @@ -38179,7 +38284,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] }, @@ -38234,7 +38339,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -38296,7 +38401,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] }, @@ -38381,7 +38486,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] }, @@ -38497,7 +38602,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -38624,7 +38729,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] }, @@ -38749,7 +38854,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -38879,7 +38984,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -38930,7 +39035,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -39078,7 +39183,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] }, @@ -39183,7 +39288,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] }, @@ -39252,7 +39357,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -39314,7 +39419,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] }, @@ -39425,7 +39530,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] }, @@ -39567,7 +39672,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -39632,7 +39737,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -39753,7 +39858,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -39815,7 +39920,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -39959,7 +40064,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -40077,7 +40182,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] }, @@ -40122,7 +40227,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -40217,7 +40322,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -40341,7 +40446,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -40454,7 +40559,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] }, @@ -40596,7 +40701,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -40633,7 +40738,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -40667,7 +40772,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -40704,7 +40809,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -40808,7 +40913,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] }, @@ -40945,7 +41050,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] }, @@ -41015,7 +41120,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -41107,7 +41212,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -41199,7 +41304,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -41236,7 +41341,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -41273,7 +41378,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -41335,7 +41440,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -41400,7 +41505,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -41463,7 +41568,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -41525,7 +41630,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -41658,7 +41763,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -41791,7 +41896,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -41939,7 +42044,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -42197,7 +42302,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -42414,7 +42519,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -42521,7 +42626,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -42572,7 +42677,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -42629,7 +42734,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] }, @@ -42775,7 +42880,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -42837,7 +42942,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] }, @@ -42948,7 +43053,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] }, @@ -43090,7 +43195,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -43156,7 +43261,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -43213,7 +43318,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -43270,7 +43375,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -47809,6 +47914,21 @@ "type": "string", "format": "date-time" }, + "id": { + "type": "string", + "format": "uuid", + "readOnly": true + }, + "flags": { + "type": "array", + "items": { + "oneOf": [ + { + "$ref": "#/components/schemas/FlagModel" + } + ] + } + }, "state": { "$ref": "#/components/schemas/DocumentVariantStateModel" }, @@ -47826,21 +47946,6 @@ "type": "string", "format": "date-time", "nullable": true - }, - "id": { - "type": "string", - "format": "uuid", - "readOnly": true - }, - "flags": { - "type": "array", - "items": { - "oneOf": [ - { - "$ref": "#/components/schemas/FlagModel" - } - ] - } } }, "additionalProperties": false @@ -48576,6 +48681,8 @@ }, "ElementVariantItemResponseModel": { "required": [ + "flags", + "id", "name", "state" ], @@ -48588,6 +48695,21 @@ "type": "string", "nullable": true }, + "id": { + "type": "string", + "format": "uuid", + "readOnly": true + }, + "flags": { + "type": "array", + "items": { + "oneOf": [ + { + "$ref": "#/components/schemas/FlagModel" + } + ] + } + }, "state": { "$ref": "#/components/schemas/DocumentVariantStateModel" } @@ -48618,6 +48740,8 @@ "ElementVariantResponseModel": { "required": [ "createDate", + "flags", + "id", "name", "state", "updateDate" @@ -48644,6 +48768,21 @@ "type": "string", "format": "date-time" }, + "id": { + "type": "string", + "format": "uuid", + "readOnly": true + }, + "flags": { + "type": "array", + "items": { + "oneOf": [ + { + "$ref": "#/components/schemas/FlagModel" + } + ] + } + }, "state": { "$ref": "#/components/schemas/DocumentVariantStateModel" }, @@ -49040,7 +49179,7 @@ }, "actionParameters": { "type": "object", - "additionalProperties": {}, + "additionalProperties": { }, "nullable": true } }, @@ -49348,6 +49487,10 @@ "nullable": true }, "nullable": true + }, + "uniqueKeyFieldName": { + "type": "string", + "nullable": true } }, "additionalProperties": false @@ -49848,7 +49991,7 @@ }, "extensions": { "type": "array", - "items": {} + "items": { } } }, "additionalProperties": false @@ -53957,7 +54100,7 @@ "nullable": true } }, - "additionalProperties": {} + "additionalProperties": { } }, "ProblemDetailsBuilderModel": { "type": "object", @@ -58488,7 +58631,7 @@ "authorizationCode": { "authorizationUrl": "/umbraco/management/api/v1/security/back-office/authorize", "tokenUrl": "/umbraco/management/api/v1/security/back-office/token", - "scopes": {} + "scopes": { } } } } diff --git a/src/Umbraco.Cms.Api.Management/Security/ConfigureBackOfficeIdentityOptions.cs b/src/Umbraco.Cms.Api.Management/Security/ConfigureBackOfficeIdentityOptions.cs index 467e2ceb08cb..bc159cdd087c 100644 --- a/src/Umbraco.Cms.Api.Management/Security/ConfigureBackOfficeIdentityOptions.cs +++ b/src/Umbraco.Cms.Api.Management/Security/ConfigureBackOfficeIdentityOptions.cs @@ -12,20 +12,29 @@ namespace Umbraco.Cms.Api.Management.Security; /// public sealed class ConfigureBackOfficeIdentityOptions : IConfigureOptions { - private readonly UserPasswordConfigurationSettings _userPasswordConfiguration; private readonly SecuritySettings _securitySettings; + /// + /// Initializes a new instance of the class with the specified user password configuration and security settings. + /// + /// The options containing security settings. + public ConfigureBackOfficeIdentityOptions( + IOptions securitySettings) + { + _securitySettings = securitySettings.Value; + } + /// /// Initializes a new instance of the class with the specified user password configuration and security settings. /// /// The options containing user password configuration settings. /// The options containing security settings. + [Obsolete("Use the constructor that only takes IOptions instead. Scheduled for removal in Umbraco 19.")] public ConfigureBackOfficeIdentityOptions( IOptions userPasswordConfiguration, IOptions securitySettings) + : this(securitySettings) { - _userPasswordConfiguration = userPasswordConfiguration.Value; - _securitySettings = securitySettings.Value; } /// @@ -50,8 +59,8 @@ public void Configure(BackOfficeIdentityOptions options) options.Lockout.AllowedForNewUsers = true; options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(_securitySettings.UserDefaultLockoutTimeInMinutes); - options.Password.ConfigurePasswordOptions(_userPasswordConfiguration); + options.Password.ConfigurePasswordOptions(_securitySettings.UserPassword); - options.Lockout.MaxFailedAccessAttempts = _userPasswordConfiguration.MaxFailedAccessAttemptsBeforeLockout; + options.Lockout.MaxFailedAccessAttempts = _securitySettings.UserPassword.MaxFailedAccessAttemptsBeforeLockout; } } diff --git a/src/Umbraco.Cms.Api.Management/Security/ForgotPasswordUriProvider.cs b/src/Umbraco.Cms.Api.Management/Security/ForgotPasswordUriProvider.cs index e0f88a6caf81..7d71500a2ce6 100644 --- a/src/Umbraco.Cms.Api.Management/Security/ForgotPasswordUriProvider.cs +++ b/src/Umbraco.Cms.Api.Management/Security/ForgotPasswordUriProvider.cs @@ -1,8 +1,5 @@ using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Routing; -using Microsoft.Extensions.Options; using Umbraco.Cms.Core; -using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.Hosting; using Umbraco.Cms.Core.Models.Membership; using Umbraco.Cms.Core.Security; @@ -16,7 +13,6 @@ namespace Umbraco.Cms.Api.Management.Security; /// public class ForgotPasswordUriProvider : IForgotPasswordUriProvider { - private readonly ICoreBackOfficeUserManager _userManager; private readonly IHostingEnvironment _hostingEnvironment; private readonly IHttpContextAccessor _httpContextAccessor; @@ -37,29 +33,37 @@ public ForgotPasswordUriProvider( _httpContextAccessor = httpContextAccessor; } + /// public async Task> CreateForgotPasswordUriAsync(IUser user) { - Attempt tokenAttempt = await _userManager.GeneratePasswordResetTokenAsync(user); + if (_httpContextAccessor.HttpContext is null) + { + throw new NotSupportedException("Needs a HttpContext"); + } - if (tokenAttempt.Success is false) + Uri? appUrl = _hostingEnvironment.ApplicationMainUrl; + if (appUrl is null) { - return Attempt.FailWithStatus(tokenAttempt.Status, new Uri(string.Empty)); + return Attempt.FailWithStatus(UserOperationStatus.ApplicationUrlNotConfigured, default!); } - HttpRequest? request = _httpContextAccessor.HttpContext?.Request; - if (request is null) + Attempt tokenAttempt = await _userManager.GeneratePasswordResetTokenAsync(user); + + if (tokenAttempt.Success is false) { - throw new NotSupportedException("Needs a HttpContext"); + return Attempt.FailWithStatus(tokenAttempt.Status, default!); } - var uriBuilder = new UriBuilder(_hostingEnvironment.ApplicationMainUrl); - uriBuilder.Path = BackOfficeLoginController.LoginPath; - uriBuilder.Query = QueryString.Create(new KeyValuePair[] + var uriBuilder = new UriBuilder(appUrl) { - new ("flow", "reset-password"), - new ("userId", user.Key.ToString()), - new ("resetCode", tokenAttempt.Result.ToUrlBase64()), - }).ToUriComponent(); + Path = BackOfficeLoginController.LoginPath, + Query = QueryString.Create(new KeyValuePair[] + { + new("flow", "reset-password"), + new("userId", user.Key.ToString()), + new("resetCode", tokenAttempt.Result.ToUrlBase64()), + }).ToUriComponent(), + }; return Attempt.SucceedWithStatus(UserOperationStatus.Success, uriBuilder.Uri); } diff --git a/src/Umbraco.Cms.Api.Management/Security/InviteUriProvider.cs b/src/Umbraco.Cms.Api.Management/Security/InviteUriProvider.cs index dc1e9637d9d0..0a890427441a 100644 --- a/src/Umbraco.Cms.Api.Management/Security/InviteUriProvider.cs +++ b/src/Umbraco.Cms.Api.Management/Security/InviteUriProvider.cs @@ -28,7 +28,6 @@ public InviteUriProvider( IHttpContextAccessor httpContextAccessor, IHostingEnvironment hostingEnvironment) { - _userManager = userManager; _httpContextAccessor = httpContextAccessor; _hostingEnvironment = hostingEnvironment; @@ -43,27 +42,34 @@ public InviteUriProvider( /// public async Task> CreateInviteUriAsync(IUser invitee) { - Attempt tokenAttempt = await _userManager.GenerateEmailConfirmationTokenAsync(invitee); + if (_httpContextAccessor.HttpContext is null) + { + throw new NotSupportedException("Needs a HttpContext"); + } - if (tokenAttempt.Success is false) + Uri? appUrl = _hostingEnvironment.ApplicationMainUrl; + if (appUrl is null) { - return Attempt.FailWithStatus(tokenAttempt.Status, new Uri(string.Empty)); + return Attempt.FailWithStatus(UserOperationStatus.ApplicationUrlNotConfigured, default!); } - HttpRequest? request = _httpContextAccessor.HttpContext?.Request; - if (request is null) + Attempt tokenAttempt = await _userManager.GenerateEmailConfirmationTokenAsync(invitee); + + if (tokenAttempt.Success is false) { - throw new NotSupportedException("Needs a HttpContext"); + return Attempt.FailWithStatus(tokenAttempt.Status, default!); } - var uriBuilder = new UriBuilder(_hostingEnvironment.ApplicationMainUrl); - uriBuilder.Path = BackOfficeLoginController.LoginPath; - uriBuilder.Query = QueryString.Create(new KeyValuePair[] + var uriBuilder = new UriBuilder(appUrl) { - new ("flow", "invite-user"), - new ("userId", invitee.Key.ToString()), - new ("inviteCode", tokenAttempt.Result.ToUrlBase64()), - }).ToUriComponent(); + Path = BackOfficeLoginController.LoginPath, + Query = QueryString.Create(new KeyValuePair[] + { + new ("flow", "invite-user"), + new ("userId", invitee.Key.ToString()), + new ("inviteCode", tokenAttempt.Result.ToUrlBase64()), + }).ToUriComponent() + }; return Attempt.SucceedWithStatus(UserOperationStatus.Success, uriBuilder.Uri); } diff --git a/src/Umbraco.Cms.Api.Management/Services/Flags/HasDocumentScheduleFlagProvider.cs b/src/Umbraco.Cms.Api.Management/Services/Flags/HasDocumentScheduleFlagProvider.cs new file mode 100644 index 000000000000..9a31ae05168e --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Services/Flags/HasDocumentScheduleFlagProvider.cs @@ -0,0 +1,49 @@ +using Umbraco.Cms.Api.Management.ViewModels; +using Umbraco.Cms.Api.Management.ViewModels.Document.Collection; +using Umbraco.Cms.Api.Management.ViewModels.Document.Item; +using Umbraco.Cms.Api.Management.ViewModels.Tree; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Services; + +namespace Umbraco.Cms.Api.Management.Services.Flags; + +/// +/// Provides flags for documents that are scheduled for publication. +/// +internal class HasDocumentScheduleFlagProvider : HasScheduleFlagProviderBase +{ + private readonly IContentService _contentService; + + public HasDocumentScheduleFlagProvider(IContentService contentService, TimeProvider timeProvider) + : base(timeProvider) + { + _contentService = contentService; + } + + /// + public override bool CanProvideFlags() => + typeof(TItem) == typeof(DocumentTreeItemResponseModel) || + typeof(TItem) == typeof(DocumentCollectionResponseModel) || + typeof(TItem) == typeof(DocumentItemResponseModel); + + /// + protected override IDictionary> GetSchedulesByKeys(Guid[] keys) + => _contentService.GetContentSchedulesByKeys(keys); + + /// + protected override void PopulateItemFlags(TItem item, ContentSchedule[] releaseSchedules) + { + switch (item) + { + case DocumentTreeItemResponseModel m: + m.Variants = PopulateVariants(m.Variants, releaseSchedules, v => v.Culture); + break; + case DocumentCollectionResponseModel m: + m.Variants = PopulateVariants(m.Variants, releaseSchedules, v => v.Culture); + break; + case DocumentItemResponseModel m: + m.Variants = PopulateVariants(m.Variants, releaseSchedules, v => v.Culture); + break; + } + } +} diff --git a/src/Umbraco.Cms.Api.Management/Services/Flags/HasElementScheduleFlagProvider.cs b/src/Umbraco.Cms.Api.Management/Services/Flags/HasElementScheduleFlagProvider.cs new file mode 100644 index 000000000000..b557e09b48f7 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Services/Flags/HasElementScheduleFlagProvider.cs @@ -0,0 +1,44 @@ +using Umbraco.Cms.Api.Management.ViewModels; +using Umbraco.Cms.Api.Management.ViewModels.Element.Item; +using Umbraco.Cms.Api.Management.ViewModels.Tree; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Services; + +namespace Umbraco.Cms.Api.Management.Services.Flags; + +/// +/// Provides flags for elements that are scheduled for publication. +/// +internal class HasElementScheduleFlagProvider : HasScheduleFlagProviderBase +{ + private readonly IElementService _elementService; + + public HasElementScheduleFlagProvider(IElementService elementService, TimeProvider timeProvider) + : base(timeProvider) + { + _elementService = elementService; + } + + /// + public override bool CanProvideFlags() => + typeof(TItem) == typeof(ElementTreeItemResponseModel) || + typeof(TItem) == typeof(ElementItemResponseModel); + + /// + protected override IDictionary> GetSchedulesByKeys(Guid[] keys) + => _elementService.GetContentSchedulesByKeys(keys); + + /// + protected override void PopulateItemFlags(TItem item, ContentSchedule[] releaseSchedules) + { + switch (item) + { + case ElementTreeItemResponseModel m: + m.Variants = PopulateVariants(m.Variants, releaseSchedules, v => v.Culture); + break; + case ElementItemResponseModel m: + m.Variants = PopulateVariants(m.Variants, releaseSchedules, v => v.Culture); + break; + } + } +} diff --git a/src/Umbraco.Cms.Api.Management/Services/Flags/HasPendingChangesFlagProvider.cs b/src/Umbraco.Cms.Api.Management/Services/Flags/HasPendingChangesFlagProvider.cs index 0a8be1d75c57..e67c2dd90996 100644 --- a/src/Umbraco.Cms.Api.Management/Services/Flags/HasPendingChangesFlagProvider.cs +++ b/src/Umbraco.Cms.Api.Management/Services/Flags/HasPendingChangesFlagProvider.cs @@ -1,11 +1,12 @@ -using Umbraco.Cms.Api.Management.ViewModels; +using Umbraco.Cms.Api.Management.ViewModels; using Umbraco.Cms.Api.Management.ViewModels.Document; +using Umbraco.Cms.Api.Management.ViewModels.Element; using Umbraco.Cms.Core; namespace Umbraco.Cms.Api.Management.Services.Flags; /// -/// Implements a that provides flags for documents that have pending changes. +/// Implements a that provides flags for documents and elements that have pending changes. /// public class HasPendingChangesFlagProvider : IFlagProvider { @@ -15,8 +16,9 @@ public class HasPendingChangesFlagProvider : IFlagProvider public bool CanProvideFlags() where TItem : IHasFlags => typeof(TItem) == typeof(DocumentVariantItemResponseModel) || - typeof(TItem) == typeof(DocumentVariantResponseModel); - + typeof(TItem) == typeof(DocumentVariantResponseModel) || + typeof(TItem) == typeof(ElementVariantItemResponseModel) || + typeof(TItem) == typeof(ElementVariantResponseModel); /// public Task PopulateFlagsAsync(IEnumerable items) @@ -24,7 +26,16 @@ public Task PopulateFlagsAsync(IEnumerable items) { foreach (TItem item in items) { - if (HasPendingChanges(item)) + DocumentVariantState? state = item switch + { + DocumentVariantItemResponseModel variant => variant.State, + DocumentVariantResponseModel variant => variant.State, + ElementVariantItemResponseModel variant => variant.State, + ElementVariantResponseModel variant => variant.State, + _ => null, + }; + + if (state == DocumentVariantState.PublishedPendingChanges) { item.AddFlag(Alias); } @@ -32,14 +43,4 @@ public Task PopulateFlagsAsync(IEnumerable items) return Task.CompletedTask; } - - /// - /// Determines if the given item has any variant that has pending changes. - /// - private static bool HasPendingChanges(object item) => item switch - { - DocumentVariantItemResponseModel variant => variant.State == DocumentVariantState.PublishedPendingChanges, - DocumentVariantResponseModel variant => variant.State == DocumentVariantState.PublishedPendingChanges, - _ => false, - }; } diff --git a/src/Umbraco.Cms.Api.Management/Services/Flags/HasScheduleFlagProvider.cs b/src/Umbraco.Cms.Api.Management/Services/Flags/HasScheduleFlagProvider.cs deleted file mode 100644 index 7d4190d5985f..000000000000 --- a/src/Umbraco.Cms.Api.Management/Services/Flags/HasScheduleFlagProvider.cs +++ /dev/null @@ -1,117 +0,0 @@ -using Umbraco.Cms.Api.Management.ViewModels; -using Umbraco.Cms.Api.Management.ViewModels.Document; -using Umbraco.Cms.Api.Management.ViewModels.Document.Collection; -using Umbraco.Cms.Api.Management.ViewModels.Document.Item; -using Umbraco.Cms.Api.Management.ViewModels.Tree; -using Umbraco.Cms.Core.Models; -using Umbraco.Cms.Core.Services; -using Constants = Umbraco.Cms.Core.Constants; - -namespace Umbraco.Cms.Api.Management.Services.Flags; - -/// -/// Implements a that provides flags for documents that are scheduled for publication. -/// -internal class HasScheduleFlagProvider : IFlagProvider -{ - private const string Alias = Constants.Conventions.Flags.Prefix + "ScheduledForPublish"; - - private readonly IContentService _contentService; - - /// - /// Initializes a new instance of the class. - /// - public HasScheduleFlagProvider(IContentService contentService) - { - _contentService = contentService; - } - - /// - public bool CanProvideFlags() - where TItem : IHasFlags => - typeof(TItem) == typeof(DocumentTreeItemResponseModel) || - typeof(TItem) == typeof(DocumentCollectionResponseModel) || - typeof(TItem) == typeof(DocumentItemResponseModel); - - /// - public Task PopulateFlagsAsync(IEnumerable items) - where TItem : IHasFlags - { - TItem[] itemsArray = items.ToArray(); - IDictionary> schedules = _contentService.GetContentSchedulesByKeys(itemsArray.Select(x => x.Id).ToArray()); - foreach (TItem item in itemsArray) - { - if (schedules.TryGetValue(item.Id, out IEnumerable? contentSchedules) is false) - { - continue; - } - - switch (item) - { - case DocumentTreeItemResponseModel documentTreeItemResponseModel: - documentTreeItemResponseModel.Variants = PopulateVariants(documentTreeItemResponseModel.Variants, contentSchedules); - break; - - case DocumentCollectionResponseModel documentCollectionResponseModel: - documentCollectionResponseModel.Variants = PopulateVariants(documentCollectionResponseModel.Variants, contentSchedules); - break; - - case DocumentItemResponseModel documentItemResponseModel: - documentItemResponseModel.Variants = PopulateVariants(documentItemResponseModel.Variants, contentSchedules); - break; - } - } - - return Task.CompletedTask; - } - - private IEnumerable PopulateVariants( - IEnumerable variants, IEnumerable schedules) - { - DocumentVariantItemResponseModel[] variantsArray = variants.ToArray(); - if (variantsArray.Length == 1) - { - DocumentVariantItemResponseModel variant = variantsArray[0]; - variant.AddFlag(Alias); - return variantsArray; - } - - foreach (DocumentVariantItemResponseModel variant in variantsArray) - { - ContentSchedule? schedule = schedules.FirstOrDefault(x => x.Culture == variant.Culture); - bool isScheduled = schedule != null && schedule.Date > DateTime.Now && string.Equals(schedule.Culture, variant.Culture); - - if (isScheduled) - { - variant.AddFlag(Alias); - } - } - - return variantsArray; - } - - private IEnumerable PopulateVariants( - IEnumerable variants, IEnumerable schedules) - { - DocumentVariantResponseModel[] variantsArray = variants.ToArray(); - if (variantsArray.Length == 1) - { - DocumentVariantResponseModel variant = variantsArray[0]; - variant.AddFlag(Alias); - return variantsArray; - } - - foreach (DocumentVariantResponseModel variant in variantsArray) - { - ContentSchedule? schedule = schedules.FirstOrDefault(x => x.Culture == variant.Culture); - bool isScheduled = schedule != null && schedule.Date > DateTime.Now && string.Equals(schedule.Culture, variant.Culture); - - if (isScheduled) - { - variant.AddFlag(Alias); - } - } - - return variantsArray; - } -} diff --git a/src/Umbraco.Cms.Api.Management/Services/Flags/HasScheduleFlagProviderBase.cs b/src/Umbraco.Cms.Api.Management/Services/Flags/HasScheduleFlagProviderBase.cs new file mode 100644 index 000000000000..d5a1da4185c5 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Services/Flags/HasScheduleFlagProviderBase.cs @@ -0,0 +1,85 @@ +using Umbraco.Cms.Api.Management.ViewModels; +using Umbraco.Cms.Core.Models; +using Constants = Umbraco.Cms.Core.Constants; + +namespace Umbraco.Cms.Api.Management.Services.Flags; + +/// +/// Base class for flag providers that add a "scheduled for publish" flag to items with active release schedules. +/// +internal abstract class HasScheduleFlagProviderBase : IFlagProvider +{ + protected const string Alias = Constants.Conventions.Flags.Prefix + "ScheduledForPublish"; + + private readonly TimeProvider _timeProvider; + + protected HasScheduleFlagProviderBase(TimeProvider timeProvider) + { + _timeProvider = timeProvider; + } + + /// + public abstract bool CanProvideFlags() + where TItem : IHasFlags; + + /// + public Task PopulateFlagsAsync(IEnumerable items) + where TItem : IHasFlags + { + TItem[] itemsArray = items.ToArray(); + Guid[] keys = itemsArray.Select(i => i.Id).ToArray(); + IDictionary> schedulesByKey = GetSchedulesByKeys(keys); + + foreach (TItem item in itemsArray) + { + if (!schedulesByKey.TryGetValue(item.Id, out IEnumerable? schedules)) + { + continue; + } + + ContentSchedule[] releaseSchedules = schedules + .Where(s => s.Action == ContentScheduleAction.Release) + .ToArray(); + + if (releaseSchedules.Length == 0) + { + continue; + } + + PopulateItemFlags(item, releaseSchedules); + } + + return Task.CompletedTask; + } + + protected abstract IDictionary> GetSchedulesByKeys(Guid[] keys); + + protected abstract void PopulateItemFlags(TItem item, ContentSchedule[] releaseSchedules) + where TItem : IHasFlags; + + protected IEnumerable PopulateVariants( + IEnumerable variants, + ContentSchedule[] schedules, + Func getCulture) + where TVariant : IHasFlags + { + TVariant[] variantsArray = variants.ToArray(); + if (variantsArray.Length == 1) + { + variantsArray[0].AddFlag(Alias); + return variantsArray; + } + + foreach (TVariant variant in variantsArray) + { + var culture = getCulture(variant); + ContentSchedule? schedule = schedules.FirstOrDefault(x => x.Culture == culture); + if (schedule is not null && schedule.Date > _timeProvider.GetUtcNow().UtcDateTime && string.Equals(schedule.Culture, culture)) + { + variant.AddFlag(Alias); + } + } + + return variantsArray; + } +} diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/Content/PublishableVariantItemResponseModelBase.cs b/src/Umbraco.Cms.Api.Management/ViewModels/Content/PublishableVariantItemResponseModelBase.cs new file mode 100644 index 000000000000..942bcfd2015c --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/ViewModels/Content/PublishableVariantItemResponseModelBase.cs @@ -0,0 +1,36 @@ +using Umbraco.Cms.Api.Management.ViewModels.Document; + +namespace Umbraco.Cms.Api.Management.ViewModels.Content; + +/// +/// Base class for publishable variant item response models, providing flag support and publish state. +/// +public abstract class PublishableVariantItemResponseModelBase : VariantItemResponseModelBase, IHasFlags +{ + private readonly List _flags = []; + + /// + public Guid Id { get; } + + /// + public IEnumerable Flags + { + get => _flags.AsEnumerable(); + set + { + _flags.Clear(); + _flags.AddRange(value); + } + } + + /// + public void AddFlag(string alias) => _flags.Add(new FlagModel { Alias = alias }); + + /// + public void RemoveFlag(string alias) => _flags.RemoveAll(x => x.Alias == alias); + + /// + /// Gets or sets the publish state of the variant. + /// + public required DocumentVariantState State { get; set; } +} diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/Content/PublishableVariantResponseModelBase.cs b/src/Umbraco.Cms.Api.Management/ViewModels/Content/PublishableVariantResponseModelBase.cs index 18c7f95a195b..bbe10df06af5 100644 --- a/src/Umbraco.Cms.Api.Management/ViewModels/Content/PublishableVariantResponseModelBase.cs +++ b/src/Umbraco.Cms.Api.Management/ViewModels/Content/PublishableVariantResponseModelBase.cs @@ -2,13 +2,50 @@ namespace Umbraco.Cms.Api.Management.ViewModels.Content; -public abstract class PublishableVariantResponseModelBase : VariantResponseModelBase +/// +/// Base class for publishable variant response models, providing flag support, publish state, and scheduling information. +/// +public abstract class PublishableVariantResponseModelBase : VariantResponseModelBase, IHasFlags { + private readonly List _flags = []; + + /// + public Guid Id { get; } + + /// + public IEnumerable Flags + { + get => _flags.AsEnumerable(); + set + { + _flags.Clear(); + _flags.AddRange(value); + } + } + + /// + public void AddFlag(string alias) => _flags.Add(new FlagModel { Alias = alias }); + + /// + public void RemoveFlag(string alias) => _flags.RemoveAll(x => x.Alias == alias); + + /// + /// Gets or sets the publish state of the variant. + /// public DocumentVariantState State { get; set; } + /// + /// Gets or sets the date the variant was published. + /// public DateTimeOffset? PublishDate { get; set; } + /// + /// Gets or sets the scheduled publish date for the variant. + /// public DateTimeOffset? ScheduledPublishDate { get; set; } + /// + /// Gets or sets the scheduled unpublish date for the variant. + /// public DateTimeOffset? ScheduledUnpublishDate { get; set; } } diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/Document/DocumentVariantItemResponseModel.cs b/src/Umbraco.Cms.Api.Management/ViewModels/Document/DocumentVariantItemResponseModel.cs index 22ad166f3fe5..9ff5e19c4805 100644 --- a/src/Umbraco.Cms.Api.Management/ViewModels/Document/DocumentVariantItemResponseModel.cs +++ b/src/Umbraco.Cms.Api.Management/ViewModels/Document/DocumentVariantItemResponseModel.cs @@ -6,37 +6,6 @@ namespace Umbraco.Cms.Api.Management.ViewModels.Document; /// Represents a response model for a document variant item in the Umbraco CMS Management API. /// A document variant typically refers to a specific language or segment version of a document. /// -public class DocumentVariantItemResponseModel : VariantItemResponseModelBase, IHasFlags +public class DocumentVariantItemResponseModel : PublishableVariantItemResponseModelBase { - private readonly List _flags = []; - - /// - /// Gets the unique identifier of the document variant. - /// - public Guid Id { get; } - - /// - /// Gets or sets the collection of flags associated with the document variant. - /// - public IEnumerable Flags - { - get => _flags.AsEnumerable(); - set - { - _flags.Clear(); - _flags.AddRange(value); - } - } - - /// - /// Adds a flag with the specified alias to the collection of flags for this document variant. - /// - /// The alias of the flag to add. - public void AddFlag(string alias) => _flags.Add(new FlagModel { Alias = alias }); - - /// Removes a flag with the specified alias from the document variant. - /// The alias of the flag to remove. - public void RemoveFlag(string alias) => _flags.RemoveAll(x => x.Alias == alias); - - public required DocumentVariantState State { get; set; } } diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/Document/DocumentVariantResponseModel.cs b/src/Umbraco.Cms.Api.Management/ViewModels/Document/DocumentVariantResponseModel.cs index d6a3e62d7d37..02f1dba4bc6e 100644 --- a/src/Umbraco.Cms.Api.Management/ViewModels/Document/DocumentVariantResponseModel.cs +++ b/src/Umbraco.Cms.Api.Management/ViewModels/Document/DocumentVariantResponseModel.cs @@ -5,37 +5,6 @@ namespace Umbraco.Cms.Api.Management.ViewModels.Document; /// /// Represents the response model returned by the API for a specific variant of a document, containing variant-specific data. /// -public class DocumentVariantResponseModel : PublishableVariantResponseModelBase, IHasFlags +public class DocumentVariantResponseModel : PublishableVariantResponseModelBase { - private readonly List _flags = []; - - /// - /// Gets the unique identifier of the document variant. - /// - public Guid Id { get; } - - /// - /// Gets or sets the collection of flags that provide additional information or status about the document variant. - /// - public IEnumerable Flags - { - get => _flags.AsEnumerable(); - set - { - _flags.Clear(); - _flags.AddRange(value); - } - } - - /// - /// Adds a flag with the specified alias to the current document variant. - /// - /// The alias of the flag to add to the document variant. - public void AddFlag(string alias) => _flags.Add(new FlagModel { Alias = alias }); - - /// - /// Removes a flag identified by the specified alias. - /// - /// The alias of the flag to remove. - public void RemoveFlag(string alias) => _flags.RemoveAll(x => x.Alias == alias); } diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/Element/ElementVariantItemResponseModel.cs b/src/Umbraco.Cms.Api.Management/ViewModels/Element/ElementVariantItemResponseModel.cs index 1875775c7bd6..f7e77dfab9df 100644 --- a/src/Umbraco.Cms.Api.Management/ViewModels/Element/ElementVariantItemResponseModel.cs +++ b/src/Umbraco.Cms.Api.Management/ViewModels/Element/ElementVariantItemResponseModel.cs @@ -1,15 +1,10 @@ -using Umbraco.Cms.Api.Management.ViewModels.Content; -using Umbraco.Cms.Api.Management.ViewModels.Document; +using Umbraco.Cms.Api.Management.ViewModels.Content; namespace Umbraco.Cms.Api.Management.ViewModels.Element; /// /// Represents a variant item response model for an element, including its publish state. /// -public class ElementVariantItemResponseModel : VariantItemResponseModelBase +public class ElementVariantItemResponseModel : PublishableVariantItemResponseModelBase { - /// - /// Gets or sets the publish state of the element variant. - /// - public required DocumentVariantState State { get; set; } } diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/Indexer/IndexResponseModel.cs b/src/Umbraco.Cms.Api.Management/ViewModels/Indexer/IndexResponseModel.cs index 62debcf0b4af..a9bf40003fa0 100644 --- a/src/Umbraco.Cms.Api.Management/ViewModels/Indexer/IndexResponseModel.cs +++ b/src/Umbraco.Cms.Api.Management/ViewModels/Indexer/IndexResponseModel.cs @@ -46,4 +46,10 @@ public class IndexResponseModel /// The properties are represented as key-value pairs. /// public IReadOnlyDictionary? ProviderProperties { get; init; } + + /// + /// Gets the name of the index field that contains the unique entity key (GUID). + /// Used by the backoffice Examine dashboard to construct edit links for search results. + /// + public string? UniqueKeyFieldName { get; init; } } diff --git a/src/Umbraco.Cms.Persistence.EFCore.SqlServer/DtoCustomization/SqlServerNodeDtoModelCustomizer.cs b/src/Umbraco.Cms.Persistence.EFCore.SqlServer/DtoCustomization/SqlServerNodeDtoModelCustomizer.cs new file mode 100644 index 000000000000..7f854824fb51 --- /dev/null +++ b/src/Umbraco.Cms.Persistence.EFCore.SqlServer/DtoCustomization/SqlServerNodeDtoModelCustomizer.cs @@ -0,0 +1,44 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; +using Umbraco.Cms.Core; +using Umbraco.Cms.Infrastructure.Persistence.Dtos.EFCore; +using Umbraco.Cms.Infrastructure.Persistence.EFCore; + +namespace Umbraco.Cms.Persistence.EFCore.SqlServer.DtoCustomization; + +/// +/// Adds SQL Server-specific included columns to indexes. +/// +public class SqlServerNodeDtoModelCustomizer : IEFCoreModelCustomizer +{ + public string? ProviderName => Constants.ProviderNames.SQLServer; + + public void Customize(EntityTypeBuilder builder) + { + // IX_umbracoNode_UniqueId + builder.HasIndex(x => x.UniqueId) + .IsUnique() + .HasDatabaseName($"IX_{NodeDto.TableName}_UniqueId") + .IncludeProperties(x => new { x.ParentId, x.Level, x.Path, x.SortOrder, x.Trashed, x.UserId, x.Text, x.CreateDate }); + + // IX_umbracoNode_parentId_nodeObjectType + builder.HasIndex(x => new { x.ParentId, x.NodeObjectType }) + .HasDatabaseName($"IX_{NodeDto.TableName}_parentId_nodeObjectType") + .IncludeProperties(x => new { x.Trashed, x.UserId, x.Level, x.Path, x.SortOrder, x.UniqueId, x.Text, x.CreateDate }); + + // IX_umbracoNode_Level + builder.HasIndex(x => new { x.Level, x.ParentId, x.SortOrder, x.NodeObjectType, x.Trashed }) + .HasDatabaseName($"IX_{NodeDto.TableName}_Level") + .IncludeProperties(x => new { x.UserId, x.Path, x.UniqueId, x.CreateDate }); + + // IX_umbracoNode_ObjectType_trashed_sorted + builder.HasIndex(x => new { x.NodeObjectType, x.Trashed, x.SortOrder, x.NodeId }) + .HasDatabaseName($"IX_{NodeDto.TableName}_ObjectType_trashed_sorted") + .IncludeProperties(x => new { x.UniqueId, x.ParentId, x.Level, x.Path, x.UserId, x.Text, x.CreateDate }); + + // IX_umbracoNode_ObjectType + builder.HasIndex(x => new { x.NodeObjectType, x.Trashed }) + .HasDatabaseName($"IX_{NodeDto.TableName}_ObjectType") + .IncludeProperties(x => new { x.UniqueId, x.ParentId, x.Level, x.Path, x.SortOrder, x.UserId, x.Text, x.CreateDate }); + } +} diff --git a/src/Umbraco.Cms.Persistence.EFCore.SqlServer/Migrations/20260326120314_AddDomainDto.Designer.cs b/src/Umbraco.Cms.Persistence.EFCore.SqlServer/Migrations/20260326120314_AddDomainDto.Designer.cs new file mode 100644 index 000000000000..ea0b7778a885 --- /dev/null +++ b/src/Umbraco.Cms.Persistence.EFCore.SqlServer/Migrations/20260326120314_AddDomainDto.Designer.cs @@ -0,0 +1,684 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Umbraco.Cms.Infrastructure.Persistence.EFCore; + +#nullable disable + +namespace Umbraco.Cms.Persistence.EFCore.SqlServer.Migrations +{ + [DbContext(typeof(UmbracoDbContext))] + [Migration("20260326120314_AddDomainDto")] + partial class AddDomainDto + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.5") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreApplication", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("nvarchar(450)"); + + b.Property("ApplicationType") + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("ClientId") + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("ClientSecret") + .HasColumnType("nvarchar(max)"); + + b.Property("ClientType") + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("ConcurrencyToken") + .IsConcurrencyToken() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("ConsentType") + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("DisplayName") + .HasColumnType("nvarchar(max)"); + + b.Property("DisplayNames") + .HasColumnType("nvarchar(max)"); + + b.Property("JsonWebKeySet") + .HasColumnType("nvarchar(max)"); + + b.Property("Permissions") + .HasColumnType("nvarchar(max)"); + + b.Property("PostLogoutRedirectUris") + .HasColumnType("nvarchar(max)"); + + b.Property("Properties") + .HasColumnType("nvarchar(max)"); + + b.Property("RedirectUris") + .HasColumnType("nvarchar(max)"); + + b.Property("Requirements") + .HasColumnType("nvarchar(max)"); + + b.Property("Settings") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("ClientId") + .IsUnique() + .HasFilter("[ClientId] IS NOT NULL"); + + b.ToTable("umbracoOpenIddictApplications", (string)null); + }); + + modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreAuthorization", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("nvarchar(450)"); + + b.Property("ApplicationId") + .HasColumnType("nvarchar(450)"); + + b.Property("ConcurrencyToken") + .IsConcurrencyToken() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("CreationDate") + .HasColumnType("datetime2"); + + b.Property("Properties") + .HasColumnType("nvarchar(max)"); + + b.Property("Scopes") + .HasColumnType("nvarchar(max)"); + + b.Property("Status") + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("Subject") + .HasMaxLength(400) + .HasColumnType("nvarchar(400)"); + + b.Property("Type") + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("ApplicationId", "Status", "Subject", "Type"); + + b.ToTable("umbracoOpenIddictAuthorizations", (string)null); + }); + + modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreScope", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("nvarchar(450)"); + + b.Property("ConcurrencyToken") + .IsConcurrencyToken() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("Description") + .HasColumnType("nvarchar(max)"); + + b.Property("Descriptions") + .HasColumnType("nvarchar(max)"); + + b.Property("DisplayName") + .HasColumnType("nvarchar(max)"); + + b.Property("DisplayNames") + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("Properties") + .HasColumnType("nvarchar(max)"); + + b.Property("Resources") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique() + .HasFilter("[Name] IS NOT NULL"); + + b.ToTable("umbracoOpenIddictScopes", (string)null); + }); + + modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreToken", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("nvarchar(450)"); + + b.Property("ApplicationId") + .HasColumnType("nvarchar(450)"); + + b.Property("AuthorizationId") + .HasColumnType("nvarchar(450)"); + + b.Property("ConcurrencyToken") + .IsConcurrencyToken() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("CreationDate") + .HasColumnType("datetime2"); + + b.Property("ExpirationDate") + .HasColumnType("datetime2"); + + b.Property("Payload") + .HasColumnType("nvarchar(max)"); + + b.Property("Properties") + .HasColumnType("nvarchar(max)"); + + b.Property("RedemptionDate") + .HasColumnType("datetime2"); + + b.Property("ReferenceId") + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("Status") + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("Subject") + .HasMaxLength(400) + .HasColumnType("nvarchar(400)"); + + b.Property("Type") + .HasMaxLength(150) + .HasColumnType("nvarchar(150)"); + + b.HasKey("Id"); + + b.HasIndex("AuthorizationId"); + + b.HasIndex("ReferenceId") + .IsUnique() + .HasFilter("[ReferenceId] IS NOT NULL"); + + b.HasIndex("ApplicationId", "Status", "Subject", "Type"); + + b.ToTable("umbracoOpenIddictTokens", (string)null); + }); + + modelBuilder.Entity("Umbraco.Cms.Infrastructure.Persistence.Dtos.EFCore.CacheInstructionDto", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasColumnName("id"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("InstructionCount") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasDefaultValue(1) + .HasColumnName("instructionCount"); + + b.Property("Instructions") + .IsRequired() + .HasColumnType("nvarchar(max)") + .HasColumnName("jsonInstruction"); + + b.Property("OriginIdentity") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("nvarchar(500)") + .HasColumnName("originated"); + + b.Property("UtcStamp") + .HasColumnType("datetime2") + .HasColumnName("utcStamp"); + + b.HasKey("Id"); + + b.ToTable("umbracoCacheInstruction", (string)null); + }); + + modelBuilder.Entity("Umbraco.Cms.Infrastructure.Persistence.Dtos.EFCore.DomainDto", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasColumnName("id"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("DefaultLanguage") + .HasColumnType("int") + .HasColumnName("domainDefaultLanguage"); + + b.Property("DomainName") + .IsRequired() + .HasColumnType("nvarchar(max)") + .HasColumnName("domainName"); + + b.Property("Key") + .HasColumnType("uniqueidentifier") + .HasColumnName("key"); + + b.Property("RootStructureId") + .HasColumnType("int") + .HasColumnName("domainRootStructureID"); + + b.Property("SortOrder") + .HasColumnType("int") + .HasColumnName("sortOrder"); + + b.HasKey("Id"); + + b.HasIndex("Key") + .IsUnique(); + + b.ToTable("umbracoDomain", (string)null); + }); + + modelBuilder.Entity("Umbraco.Cms.Infrastructure.Persistence.Dtos.EFCore.KeyValueDto", b => + { + b.Property("Key") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)") + .HasColumnName("key"); + + b.Property("UpdateDate") + .HasColumnType("datetime2") + .HasColumnName("updated"); + + b.Property("Value") + .HasColumnType("nvarchar(max)") + .HasColumnName("value"); + + b.HasKey("Key"); + + b.ToTable("umbracoKeyValue", (string)null); + }); + + modelBuilder.Entity("Umbraco.Cms.Infrastructure.Persistence.Dtos.EFCore.LanguageDto", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasColumnName("id"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CultureName") + .HasMaxLength(100) + .HasColumnType("nvarchar(100)") + .HasColumnName("languageCultureName"); + + b.Property("FallbackLanguageId") + .HasColumnType("int") + .HasColumnName("fallbackLanguageId"); + + b.Property("IsDefault") + .ValueGeneratedOnAdd() + .HasColumnType("bit") + .HasDefaultValue(false) + .HasColumnName("isDefaultVariantLang"); + + b.Property("IsMandatory") + .ValueGeneratedOnAdd() + .HasColumnType("bit") + .HasDefaultValue(false) + .HasColumnName("mandatory"); + + b.Property("IsoCode") + .HasMaxLength(14) + .HasColumnType("nvarchar(14)") + .HasColumnName("languageISOCode"); + + b.Property("LanguageKey") + .HasColumnType("uniqueidentifier") + .HasColumnName("languageKey"); + + b.HasKey("Id"); + + b.HasIndex("FallbackLanguageId"); + + b.HasIndex("IsoCode") + .IsUnique() + .HasFilter("[languageISOCode] IS NOT NULL"); + + b.HasIndex("LanguageKey") + .IsUnique(); + + b.ToTable("umbracoLanguage", (string)null); + }); + + modelBuilder.Entity("Umbraco.Cms.Infrastructure.Persistence.Dtos.EFCore.LastSyncedDto", b => + { + b.Property("MachineId") + .HasColumnType("nvarchar(450)") + .HasColumnName("machineId"); + + b.Property("LastSyncedDate") + .HasColumnType("datetime2") + .HasColumnName("lastSyncedDate"); + + b.Property("LastSyncedExternalId") + .HasColumnType("int"); + + b.Property("LastSyncedInternalId") + .HasColumnType("int") + .HasColumnName("lastSyncedInternalId"); + + b.HasKey("MachineId"); + + b.ToTable("umbracoLastSynced", (string)null); + }); + + modelBuilder.Entity("Umbraco.Cms.Infrastructure.Persistence.Dtos.EFCore.NodeDto", b => + { + b.Property("NodeId") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasColumnName("id"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("NodeId")); + + b.Property("CreateDate") + .HasColumnType("datetime2") + .HasColumnName("createDate"); + + b.Property("Level") + .HasColumnType("smallint") + .HasColumnName("level"); + + b.Property("NodeObjectType") + .HasColumnType("uniqueidentifier") + .HasColumnName("nodeObjectType"); + + b.Property("ParentId") + .HasColumnType("int") + .HasColumnName("parentId"); + + b.Property("Path") + .IsRequired() + .HasMaxLength(150) + .HasColumnType("nvarchar(150)") + .HasColumnName("path"); + + b.Property("SortOrder") + .HasColumnType("int") + .HasColumnName("sortOrder"); + + b.Property("Text") + .HasColumnType("nvarchar(max)") + .HasColumnName("text"); + + b.Property("Trashed") + .ValueGeneratedOnAdd() + .HasColumnType("bit") + .HasDefaultValue(false) + .HasColumnName("trashed"); + + b.Property("UniqueId") + .HasColumnType("uniqueidentifier") + .HasColumnName("uniqueId"); + + b.Property("UserId") + .HasColumnType("int") + .HasColumnName("nodeUser"); + + b.HasKey("NodeId"); + + b.HasIndex("Path") + .HasDatabaseName("IX_umbracoNode_Path"); + + b.HasIndex("Trashed") + .HasDatabaseName("IX_umbracoNode_Trashed"); + + b.HasIndex("UniqueId") + .IsUnique() + .HasDatabaseName("IX_umbracoNode_UniqueId"); + + SqlServerIndexBuilderExtensions.IncludeProperties(b.HasIndex("UniqueId"), new[] { "ParentId", "Level", "Path", "SortOrder", "Trashed", "UserId", "Text", "CreateDate" }); + + b.HasIndex("NodeObjectType", "Trashed") + .HasDatabaseName("IX_umbracoNode_ObjectType"); + + SqlServerIndexBuilderExtensions.IncludeProperties(b.HasIndex("NodeObjectType", "Trashed"), new[] { "UniqueId", "ParentId", "Level", "Path", "SortOrder", "UserId", "Text", "CreateDate" }); + + b.HasIndex("ParentId", "NodeObjectType") + .HasDatabaseName("IX_umbracoNode_parentId_nodeObjectType"); + + SqlServerIndexBuilderExtensions.IncludeProperties(b.HasIndex("ParentId", "NodeObjectType"), new[] { "Trashed", "UserId", "Level", "Path", "SortOrder", "UniqueId", "Text", "CreateDate" }); + + b.HasIndex("NodeObjectType", "Trashed", "SortOrder", "NodeId") + .HasDatabaseName("IX_umbracoNode_ObjectType_trashed_sorted"); + + SqlServerIndexBuilderExtensions.IncludeProperties(b.HasIndex("NodeObjectType", "Trashed", "SortOrder", "NodeId"), new[] { "UniqueId", "ParentId", "Level", "Path", "UserId", "Text", "CreateDate" }); + + b.HasIndex("Level", "ParentId", "SortOrder", "NodeObjectType", "Trashed") + .HasDatabaseName("IX_umbracoNode_Level"); + + SqlServerIndexBuilderExtensions.IncludeProperties(b.HasIndex("Level", "ParentId", "SortOrder", "NodeObjectType", "Trashed"), new[] { "UserId", "Path", "UniqueId", "CreateDate" }); + + b.ToTable("umbracoNode", (string)null); + }); + + modelBuilder.Entity("Umbraco.Cms.Infrastructure.Persistence.Dtos.EFCore.Webhook2ContentTypeKeysDto", b => + { + b.Property("WebhookId") + .HasColumnType("int") + .HasColumnName("webhookId"); + + b.Property("ContentTypeKey") + .HasColumnType("uniqueidentifier") + .HasColumnName("entityKey"); + + b.HasKey("WebhookId", "ContentTypeKey") + .HasName("PK_webhookEntityKey2Webhook"); + + b.ToTable("umbracoWebhook2ContentTypeKeys", (string)null); + }); + + modelBuilder.Entity("Umbraco.Cms.Infrastructure.Persistence.Dtos.EFCore.Webhook2EventsDto", b => + { + b.Property("WebhookId") + .HasColumnType("int") + .HasColumnName("webhookId"); + + b.Property("Event") + .HasMaxLength(255) + .HasColumnType("nvarchar(255)") + .HasColumnName("event"); + + b.HasKey("WebhookId", "Event") + .HasName("PK_webhookEvent2WebhookDto"); + + b.ToTable("umbracoWebhook2Events", (string)null); + }); + + modelBuilder.Entity("Umbraco.Cms.Infrastructure.Persistence.Dtos.EFCore.Webhook2HeadersDto", b => + { + b.Property("WebhookId") + .HasColumnType("int") + .HasColumnName("webhookId"); + + b.Property("Key") + .HasColumnType("nvarchar(450)") + .HasColumnName("Key"); + + b.Property("Value") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("WebhookId", "Key") + .HasName("PK_headers2WebhookDto"); + + b.ToTable("umbracoWebhook2Headers", (string)null); + }); + + modelBuilder.Entity("Umbraco.Cms.Infrastructure.Persistence.Dtos.EFCore.WebhookDto", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasColumnName("id"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Description") + .HasColumnType("nvarchar(max)") + .HasColumnName("description"); + + b.Property("Enabled") + .HasColumnType("bit") + .HasColumnName("enabled"); + + b.Property("Key") + .HasColumnType("uniqueidentifier") + .HasColumnName("key"); + + b.Property("Name") + .HasColumnType("nvarchar(max)") + .HasColumnName("name"); + + b.Property("Url") + .IsRequired() + .HasColumnType("nvarchar(max)") + .HasColumnName("url"); + + b.HasKey("Id"); + + b.ToTable("umbracoWebhook", (string)null); + }); + + modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreAuthorization", b => + { + b.HasOne("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreApplication", "Application") + .WithMany("Authorizations") + .HasForeignKey("ApplicationId"); + + b.Navigation("Application"); + }); + + modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreToken", b => + { + b.HasOne("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreApplication", "Application") + .WithMany("Tokens") + .HasForeignKey("ApplicationId"); + + b.HasOne("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreAuthorization", "Authorization") + .WithMany("Tokens") + .HasForeignKey("AuthorizationId"); + + b.Navigation("Application"); + + b.Navigation("Authorization"); + }); + + modelBuilder.Entity("Umbraco.Cms.Infrastructure.Persistence.Dtos.EFCore.LanguageDto", b => + { + b.HasOne("Umbraco.Cms.Infrastructure.Persistence.Dtos.EFCore.LanguageDto", "FallbackLanguage") + .WithMany() + .HasForeignKey("FallbackLanguageId") + .OnDelete(DeleteBehavior.NoAction); + + b.Navigation("FallbackLanguage"); + }); + + modelBuilder.Entity("Umbraco.Cms.Infrastructure.Persistence.Dtos.EFCore.NodeDto", b => + { + b.HasOne("Umbraco.Cms.Infrastructure.Persistence.Dtos.EFCore.NodeDto", null) + .WithMany() + .HasForeignKey("ParentId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + }); + + modelBuilder.Entity("Umbraco.Cms.Infrastructure.Persistence.Dtos.EFCore.Webhook2ContentTypeKeysDto", b => + { + b.HasOne("Umbraco.Cms.Infrastructure.Persistence.Dtos.EFCore.WebhookDto", "Webhook") + .WithMany("Webhook2ContentTypeKeys") + .HasForeignKey("WebhookId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Webhook"); + }); + + modelBuilder.Entity("Umbraco.Cms.Infrastructure.Persistence.Dtos.EFCore.Webhook2EventsDto", b => + { + b.HasOne("Umbraco.Cms.Infrastructure.Persistence.Dtos.EFCore.WebhookDto", "Webhook") + .WithMany("Webhook2Events") + .HasForeignKey("WebhookId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Webhook"); + }); + + modelBuilder.Entity("Umbraco.Cms.Infrastructure.Persistence.Dtos.EFCore.Webhook2HeadersDto", b => + { + b.HasOne("Umbraco.Cms.Infrastructure.Persistence.Dtos.EFCore.WebhookDto", "Webhook") + .WithMany("Webhook2Headers") + .HasForeignKey("WebhookId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Webhook"); + }); + + modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreApplication", b => + { + b.Navigation("Authorizations"); + + b.Navigation("Tokens"); + }); + + modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreAuthorization", b => + { + b.Navigation("Tokens"); + }); + + modelBuilder.Entity("Umbraco.Cms.Infrastructure.Persistence.Dtos.EFCore.WebhookDto", b => + { + b.Navigation("Webhook2ContentTypeKeys"); + + b.Navigation("Webhook2Events"); + + b.Navigation("Webhook2Headers"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Umbraco.Cms.Persistence.EFCore.SqlServer/Migrations/20260326120314_AddDomainDto.cs b/src/Umbraco.Cms.Persistence.EFCore.SqlServer/Migrations/20260326120314_AddDomainDto.cs new file mode 100644 index 000000000000..a48c9e4c7e3a --- /dev/null +++ b/src/Umbraco.Cms.Persistence.EFCore.SqlServer/Migrations/20260326120314_AddDomainDto.cs @@ -0,0 +1,21 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Umbraco.Cms.Persistence.EFCore.SqlServer.Migrations +{ + /// + public partial class AddDomainDto : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + } + } +} diff --git a/src/Umbraco.Cms.Persistence.EFCore.SqlServer/Migrations/UmbracoDbContextModelSnapshot.cs b/src/Umbraco.Cms.Persistence.EFCore.SqlServer/Migrations/UmbracoDbContextModelSnapshot.cs index 0464c3b88e7c..98db50aa81dc 100644 --- a/src/Umbraco.Cms.Persistence.EFCore.SqlServer/Migrations/UmbracoDbContextModelSnapshot.cs +++ b/src/Umbraco.Cms.Persistence.EFCore.SqlServer/Migrations/UmbracoDbContextModelSnapshot.cs @@ -17,7 +17,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) { #pragma warning disable 612, 618 modelBuilder - .HasAnnotation("ProductVersion", "10.0.2") + .HasAnnotation("ProductVersion", "10.0.5") .HasAnnotation("Relational:MaxIdentifierLength", 128); SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); @@ -268,6 +268,44 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("umbracoCacheInstruction", (string)null); }); + modelBuilder.Entity("Umbraco.Cms.Infrastructure.Persistence.Dtos.EFCore.DomainDto", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasColumnName("id"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("DefaultLanguage") + .HasColumnType("int") + .HasColumnName("domainDefaultLanguage"); + + b.Property("DomainName") + .IsRequired() + .HasColumnType("nvarchar(max)") + .HasColumnName("domainName"); + + b.Property("Key") + .HasColumnType("uniqueidentifier") + .HasColumnName("key"); + + b.Property("RootStructureId") + .HasColumnType("int") + .HasColumnName("domainRootStructureID"); + + b.Property("SortOrder") + .HasColumnType("int") + .HasColumnName("sortOrder"); + + b.HasKey("Id"); + + b.HasIndex("Key") + .IsUnique(); + + b.ToTable("umbracoDomain", (string)null); + }); + modelBuilder.Entity("Umbraco.Cms.Infrastructure.Persistence.Dtos.EFCore.KeyValueDto", b => { b.Property("Key") @@ -363,6 +401,96 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("umbracoLastSynced", (string)null); }); + modelBuilder.Entity("Umbraco.Cms.Infrastructure.Persistence.Dtos.EFCore.NodeDto", b => + { + b.Property("NodeId") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasColumnName("id"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("NodeId")); + + b.Property("CreateDate") + .HasColumnType("datetime2") + .HasColumnName("createDate"); + + b.Property("Level") + .HasColumnType("smallint") + .HasColumnName("level"); + + b.Property("NodeObjectType") + .HasColumnType("uniqueidentifier") + .HasColumnName("nodeObjectType"); + + b.Property("ParentId") + .HasColumnType("int") + .HasColumnName("parentId"); + + b.Property("Path") + .IsRequired() + .HasMaxLength(150) + .HasColumnType("nvarchar(150)") + .HasColumnName("path"); + + b.Property("SortOrder") + .HasColumnType("int") + .HasColumnName("sortOrder"); + + b.Property("Text") + .HasColumnType("nvarchar(max)") + .HasColumnName("text"); + + b.Property("Trashed") + .ValueGeneratedOnAdd() + .HasColumnType("bit") + .HasDefaultValue(false) + .HasColumnName("trashed"); + + b.Property("UniqueId") + .HasColumnType("uniqueidentifier") + .HasColumnName("uniqueId"); + + b.Property("UserId") + .HasColumnType("int") + .HasColumnName("nodeUser"); + + b.HasKey("NodeId"); + + b.HasIndex("Path") + .HasDatabaseName("IX_umbracoNode_Path"); + + b.HasIndex("Trashed") + .HasDatabaseName("IX_umbracoNode_Trashed"); + + b.HasIndex("UniqueId") + .IsUnique() + .HasDatabaseName("IX_umbracoNode_UniqueId"); + + SqlServerIndexBuilderExtensions.IncludeProperties(b.HasIndex("UniqueId"), new[] { "ParentId", "Level", "Path", "SortOrder", "Trashed", "UserId", "Text", "CreateDate" }); + + b.HasIndex("NodeObjectType", "Trashed") + .HasDatabaseName("IX_umbracoNode_ObjectType"); + + SqlServerIndexBuilderExtensions.IncludeProperties(b.HasIndex("NodeObjectType", "Trashed"), new[] { "UniqueId", "ParentId", "Level", "Path", "SortOrder", "UserId", "Text", "CreateDate" }); + + b.HasIndex("ParentId", "NodeObjectType") + .HasDatabaseName("IX_umbracoNode_parentId_nodeObjectType"); + + SqlServerIndexBuilderExtensions.IncludeProperties(b.HasIndex("ParentId", "NodeObjectType"), new[] { "Trashed", "UserId", "Level", "Path", "SortOrder", "UniqueId", "Text", "CreateDate" }); + + b.HasIndex("NodeObjectType", "Trashed", "SortOrder", "NodeId") + .HasDatabaseName("IX_umbracoNode_ObjectType_trashed_sorted"); + + SqlServerIndexBuilderExtensions.IncludeProperties(b.HasIndex("NodeObjectType", "Trashed", "SortOrder", "NodeId"), new[] { "UniqueId", "ParentId", "Level", "Path", "UserId", "Text", "CreateDate" }); + + b.HasIndex("Level", "ParentId", "SortOrder", "NodeObjectType", "Trashed") + .HasDatabaseName("IX_umbracoNode_Level"); + + SqlServerIndexBuilderExtensions.IncludeProperties(b.HasIndex("Level", "ParentId", "SortOrder", "NodeObjectType", "Trashed"), new[] { "UserId", "Path", "UniqueId", "CreateDate" }); + + b.ToTable("umbracoNode", (string)null); + }); + modelBuilder.Entity("Umbraco.Cms.Infrastructure.Persistence.Dtos.EFCore.Webhook2ContentTypeKeysDto", b => { b.Property("WebhookId") @@ -485,6 +613,15 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Navigation("FallbackLanguage"); }); + modelBuilder.Entity("Umbraco.Cms.Infrastructure.Persistence.Dtos.EFCore.NodeDto", b => + { + b.HasOne("Umbraco.Cms.Infrastructure.Persistence.Dtos.EFCore.NodeDto", null) + .WithMany() + .HasForeignKey("ParentId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + }); + modelBuilder.Entity("Umbraco.Cms.Infrastructure.Persistence.Dtos.EFCore.Webhook2ContentTypeKeysDto", b => { b.HasOne("Umbraco.Cms.Infrastructure.Persistence.Dtos.EFCore.WebhookDto", "Webhook") diff --git a/src/Umbraco.Cms.Persistence.EFCore.SqlServer/SqlServerEFCoreDistributedLockingMechanism.cs b/src/Umbraco.Cms.Persistence.EFCore.SqlServer/SqlServerEFCoreDistributedLockingMechanism.cs index 7aa0cc8466d6..c70968cec7b6 100644 --- a/src/Umbraco.Cms.Persistence.EFCore.SqlServer/SqlServerEFCoreDistributedLockingMechanism.cs +++ b/src/Umbraco.Cms.Persistence.EFCore.SqlServer/SqlServerEFCoreDistributedLockingMechanism.cs @@ -137,7 +137,7 @@ public override string ToString() private void ObtainReadLock() { - IEfCoreScope? scope = _parent._scopeAccessor.Value.AmbientScope; + IEFCoreScope? scope = _parent._scopeAccessor.Value.AmbientScope; if (scope is null) { @@ -171,7 +171,7 @@ private void ObtainReadLock() private void ObtainWriteLock() { - IEfCoreScope? scope = _parent._scopeAccessor.Value.AmbientScope; + IEFCoreScope? scope = _parent._scopeAccessor.Value.AmbientScope; if (scope is null) { throw new PanicException("No ambient scope"); diff --git a/src/Umbraco.Cms.Persistence.EFCore.SqlServer/SqlServerMigrationProvider.cs b/src/Umbraco.Cms.Persistence.EFCore.SqlServer/SqlServerMigrationProvider.cs index 2bba2c6ac5ac..bb1eedf33411 100644 --- a/src/Umbraco.Cms.Persistence.EFCore.SqlServer/SqlServerMigrationProvider.cs +++ b/src/Umbraco.Cms.Persistence.EFCore.SqlServer/SqlServerMigrationProvider.cs @@ -55,6 +55,7 @@ public async Task MigrateAllAsync() EFCoreMigration.AddKeyValueDto => typeof(Migrations.AddKeyValueDto), EFCoreMigration.SqliteCollation => null, // SQLite-only migration, no-op on SQL Server EFCoreMigration.AddLanguageDto => typeof(Migrations.AddLanguageDto), + EFCoreMigration.AddDomainDto => typeof(Migrations.AddDomainDto), _ => throw new ArgumentOutOfRangeException(nameof(migration), $@"Not expected migration value: {migration}") }; } diff --git a/src/Umbraco.Cms.Persistence.EFCore.SqlServer/UmbracoBuilderExtensions.cs b/src/Umbraco.Cms.Persistence.EFCore.SqlServer/UmbracoBuilderExtensions.cs index e4554d76e0de..64c084f5773b 100644 --- a/src/Umbraco.Cms.Persistence.EFCore.SqlServer/UmbracoBuilderExtensions.cs +++ b/src/Umbraco.Cms.Persistence.EFCore.SqlServer/UmbracoBuilderExtensions.cs @@ -4,6 +4,7 @@ using Umbraco.Cms.Infrastructure.Persistence.EFCore; using Umbraco.Cms.Infrastructure.Persistence.EFCore.Migrations; using Umbraco.Cms.Persistence.EFCore.Migrations; +using Umbraco.Cms.Persistence.EFCore.SqlServer.DtoCustomization; using Umbraco.Extensions; namespace Umbraco.Cms.Persistence.EFCore.SqlServer; @@ -24,6 +25,11 @@ public static IUmbracoBuilder AddUmbracoEFCoreSqlServerSupport(this IUmbracoBuil builder.AddDbContextRegistrar(); + AddCustomizers(builder); + return builder; } + + private static void AddCustomizers(IUmbracoBuilder builder) => + builder.AddEFCoreModelCustomizer(); } diff --git a/src/Umbraco.Cms.Persistence.EFCore.Sqlite/Migrations/20260326120231_AddDomainDto.Designer.cs b/src/Umbraco.Cms.Persistence.EFCore.Sqlite/Migrations/20260326120231_AddDomainDto.Designer.cs new file mode 100644 index 000000000000..c52dc03f90ae --- /dev/null +++ b/src/Umbraco.Cms.Persistence.EFCore.Sqlite/Migrations/20260326120231_AddDomainDto.Designer.cs @@ -0,0 +1,714 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Umbraco.Cms.Infrastructure.Persistence.EFCore; + +#nullable disable + +namespace Umbraco.Cms.Persistence.EFCore.Sqlite.Migrations +{ + [DbContext(typeof(UmbracoDbContext))] + [Migration("20260326120231_AddDomainDto")] + partial class AddDomainDto + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "10.0.5"); + + modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreApplication", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .UseCollation("NOCASE"); + + b.Property("ApplicationType") + .HasMaxLength(50) + .HasColumnType("TEXT") + .UseCollation("NOCASE"); + + b.Property("ClientId") + .HasMaxLength(100) + .HasColumnType("TEXT") + .UseCollation("NOCASE"); + + b.Property("ClientSecret") + .HasColumnType("TEXT") + .UseCollation("NOCASE"); + + b.Property("ClientType") + .HasMaxLength(50) + .HasColumnType("TEXT") + .UseCollation("NOCASE"); + + b.Property("ConcurrencyToken") + .IsConcurrencyToken() + .HasMaxLength(50) + .HasColumnType("TEXT") + .UseCollation("NOCASE"); + + b.Property("ConsentType") + .HasMaxLength(50) + .HasColumnType("TEXT") + .UseCollation("NOCASE"); + + b.Property("DisplayName") + .HasColumnType("TEXT") + .UseCollation("NOCASE"); + + b.Property("DisplayNames") + .HasColumnType("TEXT") + .UseCollation("NOCASE"); + + b.Property("JsonWebKeySet") + .HasColumnType("TEXT") + .UseCollation("NOCASE"); + + b.Property("Permissions") + .HasColumnType("TEXT") + .UseCollation("NOCASE"); + + b.Property("PostLogoutRedirectUris") + .HasColumnType("TEXT") + .UseCollation("NOCASE"); + + b.Property("Properties") + .HasColumnType("TEXT") + .UseCollation("NOCASE"); + + b.Property("RedirectUris") + .HasColumnType("TEXT") + .UseCollation("NOCASE"); + + b.Property("Requirements") + .HasColumnType("TEXT") + .UseCollation("NOCASE"); + + b.Property("Settings") + .HasColumnType("TEXT") + .UseCollation("NOCASE"); + + b.HasKey("Id"); + + b.HasIndex("ClientId") + .IsUnique(); + + b.ToTable("umbracoOpenIddictApplications", (string)null); + }); + + modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreAuthorization", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .UseCollation("NOCASE"); + + b.Property("ApplicationId") + .HasColumnType("TEXT") + .UseCollation("NOCASE"); + + b.Property("ConcurrencyToken") + .IsConcurrencyToken() + .HasMaxLength(50) + .HasColumnType("TEXT") + .UseCollation("NOCASE"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Properties") + .HasColumnType("TEXT") + .UseCollation("NOCASE"); + + b.Property("Scopes") + .HasColumnType("TEXT") + .UseCollation("NOCASE"); + + b.Property("Status") + .HasMaxLength(50) + .HasColumnType("TEXT") + .UseCollation("NOCASE"); + + b.Property("Subject") + .HasMaxLength(400) + .HasColumnType("TEXT") + .UseCollation("NOCASE"); + + b.Property("Type") + .HasMaxLength(50) + .HasColumnType("TEXT") + .UseCollation("NOCASE"); + + b.HasKey("Id"); + + b.HasIndex("ApplicationId", "Status", "Subject", "Type"); + + b.ToTable("umbracoOpenIddictAuthorizations", (string)null); + }); + + modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreScope", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .UseCollation("NOCASE"); + + b.Property("ConcurrencyToken") + .IsConcurrencyToken() + .HasMaxLength(50) + .HasColumnType("TEXT") + .UseCollation("NOCASE"); + + b.Property("Description") + .HasColumnType("TEXT") + .UseCollation("NOCASE"); + + b.Property("Descriptions") + .HasColumnType("TEXT") + .UseCollation("NOCASE"); + + b.Property("DisplayName") + .HasColumnType("TEXT") + .UseCollation("NOCASE"); + + b.Property("DisplayNames") + .HasColumnType("TEXT") + .UseCollation("NOCASE"); + + b.Property("Name") + .HasMaxLength(200) + .HasColumnType("TEXT") + .UseCollation("NOCASE"); + + b.Property("Properties") + .HasColumnType("TEXT") + .UseCollation("NOCASE"); + + b.Property("Resources") + .HasColumnType("TEXT") + .UseCollation("NOCASE"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("umbracoOpenIddictScopes", (string)null); + }); + + modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreToken", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .UseCollation("NOCASE"); + + b.Property("ApplicationId") + .HasColumnType("TEXT") + .UseCollation("NOCASE"); + + b.Property("AuthorizationId") + .HasColumnType("TEXT") + .UseCollation("NOCASE"); + + b.Property("ConcurrencyToken") + .IsConcurrencyToken() + .HasMaxLength(50) + .HasColumnType("TEXT") + .UseCollation("NOCASE"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("ExpirationDate") + .HasColumnType("TEXT"); + + b.Property("Payload") + .HasColumnType("TEXT") + .UseCollation("NOCASE"); + + b.Property("Properties") + .HasColumnType("TEXT") + .UseCollation("NOCASE"); + + b.Property("RedemptionDate") + .HasColumnType("TEXT"); + + b.Property("ReferenceId") + .HasMaxLength(100) + .HasColumnType("TEXT") + .UseCollation("NOCASE"); + + b.Property("Status") + .HasMaxLength(50) + .HasColumnType("TEXT") + .UseCollation("NOCASE"); + + b.Property("Subject") + .HasMaxLength(400) + .HasColumnType("TEXT") + .UseCollation("NOCASE"); + + b.Property("Type") + .HasMaxLength(150) + .HasColumnType("TEXT") + .UseCollation("NOCASE"); + + b.HasKey("Id"); + + b.HasIndex("AuthorizationId"); + + b.HasIndex("ReferenceId") + .IsUnique(); + + b.HasIndex("ApplicationId", "Status", "Subject", "Type"); + + b.ToTable("umbracoOpenIddictTokens", (string)null); + }); + + modelBuilder.Entity("Umbraco.Cms.Infrastructure.Persistence.Dtos.EFCore.CacheInstructionDto", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("id"); + + b.Property("InstructionCount") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(1) + .HasColumnName("instructionCount"); + + b.Property("Instructions") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("jsonInstruction") + .UseCollation("NOCASE"); + + b.Property("OriginIdentity") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("TEXT") + .HasColumnName("originated") + .UseCollation("NOCASE"); + + b.Property("UtcStamp") + .HasColumnType("TEXT") + .HasColumnName("utcStamp"); + + b.HasKey("Id"); + + b.ToTable("umbracoCacheInstruction", (string)null); + }); + + modelBuilder.Entity("Umbraco.Cms.Infrastructure.Persistence.Dtos.EFCore.DomainDto", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("id"); + + b.Property("DefaultLanguage") + .HasColumnType("INTEGER") + .HasColumnName("domainDefaultLanguage"); + + b.Property("DomainName") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("domainName") + .UseCollation("NOCASE"); + + b.Property("Key") + .HasColumnType("TEXT") + .HasColumnName("key"); + + b.Property("RootStructureId") + .HasColumnType("INTEGER") + .HasColumnName("domainRootStructureID"); + + b.Property("SortOrder") + .HasColumnType("INTEGER") + .HasColumnName("sortOrder"); + + b.HasKey("Id"); + + b.HasIndex("Key") + .IsUnique(); + + b.ToTable("umbracoDomain", (string)null); + }); + + modelBuilder.Entity("Umbraco.Cms.Infrastructure.Persistence.Dtos.EFCore.KeyValueDto", b => + { + b.Property("Key") + .HasMaxLength(256) + .HasColumnType("TEXT") + .HasColumnName("key") + .UseCollation("NOCASE"); + + b.Property("UpdateDate") + .HasColumnType("TEXT") + .HasColumnName("updated"); + + b.Property("Value") + .HasColumnType("TEXT") + .HasColumnName("value") + .UseCollation("NOCASE"); + + b.HasKey("Key"); + + b.ToTable("umbracoKeyValue", (string)null); + }); + + modelBuilder.Entity("Umbraco.Cms.Infrastructure.Persistence.Dtos.EFCore.LanguageDto", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("id"); + + b.Property("CultureName") + .HasMaxLength(100) + .HasColumnType("TEXT") + .HasColumnName("languageCultureName") + .UseCollation("NOCASE"); + + b.Property("FallbackLanguageId") + .HasColumnType("INTEGER") + .HasColumnName("fallbackLanguageId"); + + b.Property("IsDefault") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(false) + .HasColumnName("isDefaultVariantLang"); + + b.Property("IsMandatory") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(false) + .HasColumnName("mandatory"); + + b.Property("IsoCode") + .HasMaxLength(14) + .HasColumnType("TEXT") + .HasColumnName("languageISOCode") + .UseCollation("NOCASE"); + + b.Property("LanguageKey") + .HasColumnType("TEXT") + .HasColumnName("languageKey"); + + b.HasKey("Id"); + + b.HasIndex("FallbackLanguageId"); + + b.HasIndex("IsoCode") + .IsUnique(); + + b.HasIndex("LanguageKey") + .IsUnique(); + + b.ToTable("umbracoLanguage", (string)null); + }); + + modelBuilder.Entity("Umbraco.Cms.Infrastructure.Persistence.Dtos.EFCore.LastSyncedDto", b => + { + b.Property("MachineId") + .HasColumnType("TEXT") + .HasColumnName("machineId") + .UseCollation("NOCASE"); + + b.Property("LastSyncedDate") + .HasColumnType("TEXT") + .HasColumnName("lastSyncedDate"); + + b.Property("LastSyncedExternalId") + .HasColumnType("INTEGER"); + + b.Property("LastSyncedInternalId") + .HasColumnType("INTEGER") + .HasColumnName("lastSyncedInternalId"); + + b.HasKey("MachineId"); + + b.ToTable("umbracoLastSynced", (string)null); + }); + + modelBuilder.Entity("Umbraco.Cms.Infrastructure.Persistence.Dtos.EFCore.NodeDto", b => + { + b.Property("NodeId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("id"); + + b.Property("CreateDate") + .HasColumnType("TEXT") + .HasColumnName("createDate"); + + b.Property("Level") + .HasColumnType("INTEGER") + .HasColumnName("level"); + + b.Property("NodeObjectType") + .HasColumnType("TEXT") + .HasColumnName("nodeObjectType"); + + b.Property("ParentId") + .HasColumnType("INTEGER") + .HasColumnName("parentId"); + + b.Property("Path") + .IsRequired() + .HasMaxLength(150) + .HasColumnType("TEXT") + .HasColumnName("path") + .UseCollation("NOCASE"); + + b.Property("SortOrder") + .HasColumnType("INTEGER") + .HasColumnName("sortOrder"); + + b.Property("Text") + .HasColumnType("TEXT") + .HasColumnName("text") + .UseCollation("NOCASE"); + + b.Property("Trashed") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(false) + .HasColumnName("trashed"); + + b.Property("UniqueId") + .HasColumnType("TEXT") + .HasColumnName("uniqueId"); + + b.Property("UserId") + .HasColumnType("INTEGER") + .HasColumnName("nodeUser"); + + b.HasKey("NodeId"); + + b.HasIndex("Path") + .HasDatabaseName("IX_umbracoNode_Path"); + + b.HasIndex("Trashed") + .HasDatabaseName("IX_umbracoNode_Trashed"); + + b.HasIndex("UniqueId") + .IsUnique() + .HasDatabaseName("IX_umbracoNode_UniqueId"); + + b.HasIndex("NodeObjectType", "Trashed") + .HasDatabaseName("IX_umbracoNode_ObjectType"); + + b.HasIndex("ParentId", "NodeObjectType") + .HasDatabaseName("IX_umbracoNode_parentId_nodeObjectType"); + + b.HasIndex("NodeObjectType", "Trashed", "SortOrder", "NodeId") + .HasDatabaseName("IX_umbracoNode_ObjectType_trashed_sorted"); + + b.HasIndex("Level", "ParentId", "SortOrder", "NodeObjectType", "Trashed") + .HasDatabaseName("IX_umbracoNode_Level"); + + b.ToTable("umbracoNode", (string)null); + }); + + modelBuilder.Entity("Umbraco.Cms.Infrastructure.Persistence.Dtos.EFCore.Webhook2ContentTypeKeysDto", b => + { + b.Property("WebhookId") + .HasColumnType("INTEGER") + .HasColumnName("webhookId"); + + b.Property("ContentTypeKey") + .HasColumnType("TEXT") + .HasColumnName("entityKey"); + + b.HasKey("WebhookId", "ContentTypeKey") + .HasName("PK_webhookEntityKey2Webhook"); + + b.ToTable("umbracoWebhook2ContentTypeKeys", (string)null); + }); + + modelBuilder.Entity("Umbraco.Cms.Infrastructure.Persistence.Dtos.EFCore.Webhook2EventsDto", b => + { + b.Property("WebhookId") + .HasColumnType("INTEGER") + .HasColumnName("webhookId"); + + b.Property("Event") + .HasMaxLength(255) + .HasColumnType("TEXT") + .HasColumnName("event") + .UseCollation("NOCASE"); + + b.HasKey("WebhookId", "Event") + .HasName("PK_webhookEvent2WebhookDto"); + + b.ToTable("umbracoWebhook2Events", (string)null); + }); + + modelBuilder.Entity("Umbraco.Cms.Infrastructure.Persistence.Dtos.EFCore.Webhook2HeadersDto", b => + { + b.Property("WebhookId") + .HasColumnType("INTEGER") + .HasColumnName("webhookId"); + + b.Property("Key") + .HasColumnType("TEXT") + .HasColumnName("Key") + .UseCollation("NOCASE"); + + b.Property("Value") + .IsRequired() + .HasColumnType("TEXT") + .UseCollation("NOCASE"); + + b.HasKey("WebhookId", "Key") + .HasName("PK_headers2WebhookDto"); + + b.ToTable("umbracoWebhook2Headers", (string)null); + }); + + modelBuilder.Entity("Umbraco.Cms.Infrastructure.Persistence.Dtos.EFCore.WebhookDto", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("id"); + + b.Property("Description") + .HasColumnType("TEXT") + .HasColumnName("description") + .UseCollation("NOCASE"); + + b.Property("Enabled") + .HasColumnType("INTEGER") + .HasColumnName("enabled"); + + b.Property("Key") + .HasColumnType("TEXT") + .HasColumnName("key"); + + b.Property("Name") + .HasColumnType("TEXT") + .HasColumnName("name") + .UseCollation("NOCASE"); + + b.Property("Url") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("url") + .UseCollation("NOCASE"); + + b.HasKey("Id"); + + b.ToTable("umbracoWebhook", (string)null); + }); + + modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreAuthorization", b => + { + b.HasOne("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreApplication", "Application") + .WithMany("Authorizations") + .HasForeignKey("ApplicationId"); + + b.Navigation("Application"); + }); + + modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreToken", b => + { + b.HasOne("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreApplication", "Application") + .WithMany("Tokens") + .HasForeignKey("ApplicationId"); + + b.HasOne("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreAuthorization", "Authorization") + .WithMany("Tokens") + .HasForeignKey("AuthorizationId"); + + b.Navigation("Application"); + + b.Navigation("Authorization"); + }); + + modelBuilder.Entity("Umbraco.Cms.Infrastructure.Persistence.Dtos.EFCore.LanguageDto", b => + { + b.HasOne("Umbraco.Cms.Infrastructure.Persistence.Dtos.EFCore.LanguageDto", "FallbackLanguage") + .WithMany() + .HasForeignKey("FallbackLanguageId") + .OnDelete(DeleteBehavior.NoAction); + + b.Navigation("FallbackLanguage"); + }); + + modelBuilder.Entity("Umbraco.Cms.Infrastructure.Persistence.Dtos.EFCore.NodeDto", b => + { + b.HasOne("Umbraco.Cms.Infrastructure.Persistence.Dtos.EFCore.NodeDto", null) + .WithMany() + .HasForeignKey("ParentId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + }); + + modelBuilder.Entity("Umbraco.Cms.Infrastructure.Persistence.Dtos.EFCore.Webhook2ContentTypeKeysDto", b => + { + b.HasOne("Umbraco.Cms.Infrastructure.Persistence.Dtos.EFCore.WebhookDto", "Webhook") + .WithMany("Webhook2ContentTypeKeys") + .HasForeignKey("WebhookId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Webhook"); + }); + + modelBuilder.Entity("Umbraco.Cms.Infrastructure.Persistence.Dtos.EFCore.Webhook2EventsDto", b => + { + b.HasOne("Umbraco.Cms.Infrastructure.Persistence.Dtos.EFCore.WebhookDto", "Webhook") + .WithMany("Webhook2Events") + .HasForeignKey("WebhookId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Webhook"); + }); + + modelBuilder.Entity("Umbraco.Cms.Infrastructure.Persistence.Dtos.EFCore.Webhook2HeadersDto", b => + { + b.HasOne("Umbraco.Cms.Infrastructure.Persistence.Dtos.EFCore.WebhookDto", "Webhook") + .WithMany("Webhook2Headers") + .HasForeignKey("WebhookId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Webhook"); + }); + + modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreApplication", b => + { + b.Navigation("Authorizations"); + + b.Navigation("Tokens"); + }); + + modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreAuthorization", b => + { + b.Navigation("Tokens"); + }); + + modelBuilder.Entity("Umbraco.Cms.Infrastructure.Persistence.Dtos.EFCore.WebhookDto", b => + { + b.Navigation("Webhook2ContentTypeKeys"); + + b.Navigation("Webhook2Events"); + + b.Navigation("Webhook2Headers"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Umbraco.Cms.Persistence.EFCore.Sqlite/Migrations/20260326120231_AddDomainDto.cs b/src/Umbraco.Cms.Persistence.EFCore.Sqlite/Migrations/20260326120231_AddDomainDto.cs new file mode 100644 index 000000000000..a6aa9359b4f5 --- /dev/null +++ b/src/Umbraco.Cms.Persistence.EFCore.Sqlite/Migrations/20260326120231_AddDomainDto.cs @@ -0,0 +1,21 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Umbraco.Cms.Persistence.EFCore.Sqlite.Migrations +{ + /// + public partial class AddDomainDto : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + } + } +} diff --git a/src/Umbraco.Cms.Persistence.EFCore.Sqlite/Migrations/UmbracoDbContextModelSnapshot.cs b/src/Umbraco.Cms.Persistence.EFCore.Sqlite/Migrations/UmbracoDbContextModelSnapshot.cs index 237e00255f5f..4ff8b61e8c56 100644 --- a/src/Umbraco.Cms.Persistence.EFCore.Sqlite/Migrations/UmbracoDbContextModelSnapshot.cs +++ b/src/Umbraco.Cms.Persistence.EFCore.Sqlite/Migrations/UmbracoDbContextModelSnapshot.cs @@ -15,7 +15,7 @@ partial class UmbracoDbContextModelSnapshot : ModelSnapshot protected override void BuildModel(ModelBuilder modelBuilder) { #pragma warning disable 612, 618 - modelBuilder.HasAnnotation("ProductVersion", "10.0.2"); + modelBuilder.HasAnnotation("ProductVersion", "10.0.5"); modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreApplication", b => { @@ -303,6 +303,43 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("umbracoCacheInstruction", (string)null); }); + modelBuilder.Entity("Umbraco.Cms.Infrastructure.Persistence.Dtos.EFCore.DomainDto", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("id"); + + b.Property("DefaultLanguage") + .HasColumnType("INTEGER") + .HasColumnName("domainDefaultLanguage"); + + b.Property("DomainName") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("domainName") + .UseCollation("NOCASE"); + + b.Property("Key") + .HasColumnType("TEXT") + .HasColumnName("key"); + + b.Property("RootStructureId") + .HasColumnType("INTEGER") + .HasColumnName("domainRootStructureID"); + + b.Property("SortOrder") + .HasColumnType("INTEGER") + .HasColumnName("sortOrder"); + + b.HasKey("Id"); + + b.HasIndex("Key") + .IsUnique(); + + b.ToTable("umbracoDomain", (string)null); + }); + modelBuilder.Entity("Umbraco.Cms.Infrastructure.Persistence.Dtos.EFCore.KeyValueDto", b => { b.Property("Key") @@ -400,6 +437,86 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("umbracoLastSynced", (string)null); }); + modelBuilder.Entity("Umbraco.Cms.Infrastructure.Persistence.Dtos.EFCore.NodeDto", b => + { + b.Property("NodeId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("id"); + + b.Property("CreateDate") + .HasColumnType("TEXT") + .HasColumnName("createDate"); + + b.Property("Level") + .HasColumnType("INTEGER") + .HasColumnName("level"); + + b.Property("NodeObjectType") + .HasColumnType("TEXT") + .HasColumnName("nodeObjectType"); + + b.Property("ParentId") + .HasColumnType("INTEGER") + .HasColumnName("parentId"); + + b.Property("Path") + .IsRequired() + .HasMaxLength(150) + .HasColumnType("TEXT") + .HasColumnName("path") + .UseCollation("NOCASE"); + + b.Property("SortOrder") + .HasColumnType("INTEGER") + .HasColumnName("sortOrder"); + + b.Property("Text") + .HasColumnType("TEXT") + .HasColumnName("text") + .UseCollation("NOCASE"); + + b.Property("Trashed") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(false) + .HasColumnName("trashed"); + + b.Property("UniqueId") + .HasColumnType("TEXT") + .HasColumnName("uniqueId"); + + b.Property("UserId") + .HasColumnType("INTEGER") + .HasColumnName("nodeUser"); + + b.HasKey("NodeId"); + + b.HasIndex("Path") + .HasDatabaseName("IX_umbracoNode_Path"); + + b.HasIndex("Trashed") + .HasDatabaseName("IX_umbracoNode_Trashed"); + + b.HasIndex("UniqueId") + .IsUnique() + .HasDatabaseName("IX_umbracoNode_UniqueId"); + + b.HasIndex("NodeObjectType", "Trashed") + .HasDatabaseName("IX_umbracoNode_ObjectType"); + + b.HasIndex("ParentId", "NodeObjectType") + .HasDatabaseName("IX_umbracoNode_parentId_nodeObjectType"); + + b.HasIndex("NodeObjectType", "Trashed", "SortOrder", "NodeId") + .HasDatabaseName("IX_umbracoNode_ObjectType_trashed_sorted"); + + b.HasIndex("Level", "ParentId", "SortOrder", "NodeObjectType", "Trashed") + .HasDatabaseName("IX_umbracoNode_Level"); + + b.ToTable("umbracoNode", (string)null); + }); + modelBuilder.Entity("Umbraco.Cms.Infrastructure.Persistence.Dtos.EFCore.Webhook2ContentTypeKeysDto", b => { b.Property("WebhookId") @@ -526,6 +643,15 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Navigation("FallbackLanguage"); }); + modelBuilder.Entity("Umbraco.Cms.Infrastructure.Persistence.Dtos.EFCore.NodeDto", b => + { + b.HasOne("Umbraco.Cms.Infrastructure.Persistence.Dtos.EFCore.NodeDto", null) + .WithMany() + .HasForeignKey("ParentId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + }); + modelBuilder.Entity("Umbraco.Cms.Infrastructure.Persistence.Dtos.EFCore.Webhook2ContentTypeKeysDto", b => { b.HasOne("Umbraco.Cms.Infrastructure.Persistence.Dtos.EFCore.WebhookDto", "Webhook") diff --git a/src/Umbraco.Cms.Persistence.EFCore.Sqlite/SqliteEFCoreDistributedLockingMechanism.cs b/src/Umbraco.Cms.Persistence.EFCore.Sqlite/SqliteEFCoreDistributedLockingMechanism.cs index 37e7565eeb08..293caefb849e 100644 --- a/src/Umbraco.Cms.Persistence.EFCore.Sqlite/SqliteEFCoreDistributedLockingMechanism.cs +++ b/src/Umbraco.Cms.Persistence.EFCore.Sqlite/SqliteEFCoreDistributedLockingMechanism.cs @@ -142,7 +142,7 @@ public override string ToString() // Mostly no-op just check that we didn't end up ReadUncommitted for real. private void ObtainReadLock() { - IEfCoreScope? efCoreScope = _parent._efCoreScopeAccessor.Value.AmbientScope + IEFCoreScope? efCoreScope = _parent._efCoreScopeAccessor.Value.AmbientScope ?? throw new PanicException("No current ambient scope"); efCoreScope.ExecuteWithContextAsync(database => @@ -160,7 +160,7 @@ private void ObtainReadLock() // lock occurs for entire database as opposed to row/table. private void ObtainWriteLock() { - IEfCoreScope? efCoreScope = _parent._efCoreScopeAccessor.Value.AmbientScope + IEFCoreScope? efCoreScope = _parent._efCoreScopeAccessor.Value.AmbientScope ?? throw new PanicException("No ambient scope"); efCoreScope.ExecuteWithContextAsync(async database => diff --git a/src/Umbraco.Cms.Persistence.SqlServer/Services/SqlServerSyntaxProvider.cs b/src/Umbraco.Cms.Persistence.SqlServer/Services/SqlServerSyntaxProvider.cs index bea25ce08b1b..fdfe637f95de 100644 --- a/src/Umbraco.Cms.Persistence.SqlServer/Services/SqlServerSyntaxProvider.cs +++ b/src/Umbraco.Cms.Persistence.SqlServer/Services/SqlServerSyntaxProvider.cs @@ -466,6 +466,17 @@ public SqlInspectionUtilities() #endregion + /// + public override string CreateTempTable(string tableName, string columnDefinitionSql) + => $"CREATE TABLE #{tableName} ({columnDefinitionSql})"; + + /// + public override string TempTableName(string baseName) => $"#{baseName}"; + + /// + public override string DropTempTable(string tableName) + => $"DROP TABLE IF EXISTS #{tableName}"; + private sealed class SqlPrimaryKey { public string Name { get; set; } = null!; diff --git a/src/Umbraco.Cms.Persistence.Sqlite/Services/SqliteSyntaxProvider.cs b/src/Umbraco.Cms.Persistence.Sqlite/Services/SqliteSyntaxProvider.cs index a206ee6dac2c..535afb96a21a 100644 --- a/src/Umbraco.Cms.Persistence.Sqlite/Services/SqliteSyntaxProvider.cs +++ b/src/Umbraco.Cms.Persistence.Sqlite/Services/SqliteSyntaxProvider.cs @@ -507,6 +507,10 @@ private sealed class IndexMeta public bool IsUnique { get; set; } } + /// + public override string CreateTempTable(string tableName, string columnDefinitionSql) + => $"CREATE TEMP TABLE {tableName} ({columnDefinitionSql})"; + /// public override string Length => "length"; diff --git a/src/Umbraco.Cms.StaticAssets/umbraco/UmbracoBackOffice/Index.cshtml b/src/Umbraco.Cms.StaticAssets/umbraco/UmbracoBackOffice/Index.cshtml index 671f578060c0..a665aa4cf616 100644 --- a/src/Umbraco.Cms.StaticAssets/umbraco/UmbracoBackOffice/Index.cshtml +++ b/src/Umbraco.Cms.StaticAssets/umbraco/UmbracoBackOffice/Index.cshtml @@ -34,7 +34,7 @@ Umbraco - + @await Html.BackOfficeImportMapScriptAsync(JsonSerializer, BackOfficePathGenerator, PackageManifestService, CspNonceService) diff --git a/src/Umbraco.Cms.StaticAssets/umbraco/UmbracoLogin/Index.cshtml b/src/Umbraco.Cms.StaticAssets/umbraco/UmbracoLogin/Index.cshtml index 7b08860108cf..995e13dfa292 100644 --- a/src/Umbraco.Cms.StaticAssets/umbraco/UmbracoLogin/Index.cshtml +++ b/src/Umbraco.Cms.StaticAssets/umbraco/UmbracoLogin/Index.cshtml @@ -46,7 +46,7 @@ Umbraco - +