Compare commits

...
Author SHA1 Message Date
Andy Butland d0fc7dc8a0 Tiptap RTE: Ignore no-op transactions in onUpdate to prevent phantom dirty state (closes #22767) (#22781)
Ignore Tiptap no-op transactions in onUpdate to prevent phantom dirty state.
2026-06-05 15:56:23 +02:00
Andy Butland 89baa9482b Background Jobs: Resolve server role so recurring jobs run when no application URL is configured (#23033)
Resolve server role when no application URL is configured.
2026-06-04 06:40:00 +02:00
Jacob Overgaard b3666dad8b build(deps): bumps @umbraco-ui/uui to 1.18.0 2026-06-02 12:43:21 +02:00
Sven GeusensandAndy Butland 28a403361e Developer Experience: Improve cohost editor polyfill to work with dotnet watch (closes #22773) (#22999)
* Improve cohost polyfill

* Apply suggestions from code review

Co-authored-by: Andy Butland <abutland73@gmail.com>

* Move <target/> part of the polyfill to targets file.

---------

Co-authored-by: Andy Butland <abutland73@gmail.com>
2026-06-01 14:38:35 +02:00
Jacob OvergaardandClaude Opus 4.7 8e6a791de0 Backoffice: Drop redundant search manifest import from Storybook preview
`core/manifests.ts` already imports and spreads `core/search/manifests.ts`
into its aggregate (line 23 + 58), so importing `searchManifests`
separately in `.storybook/preview.js` and spreading it next to
`coreManifests` registered the same manifests twice. Remove the redundant
import and spread.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 08:47:38 +02:00
Jacob OvergaardandClaude Opus 4.7 5ffea3152b Backoffice: Repoint Storybook preview imports at umbraco-package.ts
PR #22957 deleted every package's `manifests.ts` and consolidated the
exports into `umbraco-package.ts`, but `.storybook/preview.js` still
imported from the old paths. The result was a Vite resolve error during
`npm run build-storybook` (first failure: "Could not resolve
../src/packages/block/manifests from .storybook/preview.js").

37 import paths swapped from `…/<pkg>/manifests` to
`…/<pkg>/umbraco-package`. The two packages that still expose their
manifests via a standalone `manifests.ts` — `core` and `core/search` —
are left untouched.

Verified by `npm run build-storybook` — succeeds.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 08:43:38 +02:00
Jacob OvergaardandClaude Opus 4.7 2043ff1dbd Tiptap: Fix dead manifests.js import in the input-tiptap story
PR #22995 added `input-tiptap.stories.ts` with an import from
`'../../manifests.js'`, but PR #22957 (already on release/17.5.0) had
deleted that file and moved the `manifests` array into
`umbraco-package.ts`. The merge into release/17.5.0 didn't catch the dead
import, so Storybook 404s on the story load.

Point the import at the new home — `manifests` is still exported by name,
so this is a one-line path fix.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 08:40:17 +02:00
d28507e2e5 Migrations: Convert all sibling RTE blocks (closes #22979) (#22980)
* Fix migration of embedded block data when blocks are direct siblings in the 13 RTE source code.

* Apply suggestions from code review to update comments.

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* Address review: keep RteBlockHelper in original namespace; tidy docs and comment

- Move RteBlockHelper back to Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_15_0_0.LocalLinks
  to avoid a binary breaking change within the obsolete window (scheduled removal in v18).
  Kept as its own file rather than reverting it into LocalLinkRteProcessor.cs.
- Add a <remarks> note on ConvertBlockUdisToKeys explaining that blocks with malformed UDIs
  are dropped rather than preserved.
- Replace the opaque "fix recursive hiccup" comment in LocalLinkRteProcessor with one that
  describes what the line actually does.
- Move RteBlockHelperTests back to mirror the production namespace.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 07:45:01 +02:00
ca28195b7c Tiptap: Load enabled extensions in parallel and inline manifest APIs (#22995)
* Tiptap: Load enabled extensions in parallel and inline manifest APIs

Replace the for…of/await loop in umb-input-tiptap's #loadExtensions with
Promise.all over .map, so all enabled Tiptap extension APIs are fetched
in parallel. Configured-extension order in _extensions is preserved.

Inline the first-party Tiptap manifest API references: every
`api: () => import('./X.tiptap-api.js')` and the equivalent toolbar /
statusbar / kind references now use a static top-of-file import and
`api: ClassName`. The dynamic `await import('rich-text-essentials.tiptap-api.js')`
fallback in input-tiptap.element.ts is inlined for the same reason.

External (plugin-supplied) Tiptap extensions and the lazy modal/toolbar
UI element imports are unchanged.

Why: on Umbraco Cloud, opening a document workspace with a rich text
editor takes ~16 s uncached, of which ~14.6 s is a single serial
waterfall — 31 extension APIs fetched one after the other from a
for…of await loop, ~170 ms RTT stacked. Replacing the loop with
Promise.all collapses that to roughly one round-trip; eagerly bundling
the first-party manifests removes the dynamic chunk explosion that made
the waterfall so long in the first place. The toolbar APIs (~20 of them)
already load in a sub-100 ms parallel burst against the same server,
confirming HTTP/2 multiplexing handles bulk parallel requests fine.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Tiptap: Inline element references for toolbar/statusbar/modal/clipboard manifests

Extends the manifest-inlining pass to the remaining `element: () => import(...)`
and runtime API loader sites in the Tiptap package — toolbar/menu/action-button
kinds, the table & character-map & anchor modals, the colour-picker button, the
property-editor configuration UIs, both clipboard translators, the style-menu
kind, and the default toolbar API fallback in tiptap-toolbar.element.ts.

Result on the same Cloud test site (uncached, 17.5-rc):
  Tiptap chunk count: 71 → 4
  Total tiptap bytes: ~3.2 MB → ~3.1 MB (essentially unchanged)
  Phase 5 of the load — the serial extension chain — collapses to a single
  consolidated chunk fetch.

`input-tiptap.element.ts` and `property-editor-ui-tiptap.element.ts` are
intentionally not inlined into anything else: `<umb-input-tiptap>` is a public
element usable standalone (custom dashboards, workspace views), and the
property-editor shell loads via the property-editor UI loader. They remain
exported as their own modules.

CLAUDE.md updated to document the new convention for first-party Tiptap
extensions (direct class refs) and the carve-out for external plugin
extensions that may keep `() => import(...)` to ship their API code in a
separate chunk.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Tiptap: Move extension APIs and elements into a shared lazy boundary chunk

The previous PR collapsed ~70 Tiptap chunks into 3 by inlining first-party API
and element references directly into manifest files. That win came with a real
downside flagged in code review (lke / mra): the API/element implementation
bytes ended up in the manifest registration bundle, so every workspace —
including ones without an RTE — paid ~700 KB of Tiptap code on boot.

This commit keeps the chunk-coalescing win but restores the lazy boundary by
routing every first-party manifest's `api` / `element` reference through a
single shared bundle file `extensions/extension-apis.bundle.ts`. Each manifest
holds a dynamic-import thunk pointing at that one bundle, so:

- Rollup still emits a single chunk for all Tiptap extension code (no chunk
  explosion).
- The manifest registration bundle stays slim — it carries only metadata
  (alias / label / icon / group / kind / forExtensions) plus the thunks.
- The bundle is only fetched the first time `<umb-input-tiptap>` actually
  mounts.

Data-type configuration UIs (`extensions-configuration`,
`toolbar-configuration`, `statusbar-configuration`) read manifest metadata
via `umbExtensionsRegistry.byType(...)` only — they never call
`loadManifestApi` / `loadManifestElement`, so the data-type editor continues
to work without loading any Tiptap implementation code.

Property-editor UI elements (`tiptap-rte`, the three configuration UIs) also
revert to `() => import('./X.element.js')` so each loads on demand from its
own chunk rather than being inlined into the manifest bundle.

`umb-input-tiptap` no longer statically imports the Rich Text Essentials API;
it prepends the alias to the observed list instead, so essentials resolves
through the same lazy bundle as every other extension.

Added a test and stories file that mount `<umb-input-tiptap>` standalone (no
property-editor wrapper) to make the public usage pattern explicit.

Built and verified via `npm run build:for:cms`:
- `dist-cms/packages/tiptap/manifests.js`           48 KB  (eager at boot)
- `dist-cms/packages/tiptap/extension-apis.bundle-*.js` 84 KB  (lazy)
- `dist-cms/packages/tiptap/tiptap-toolbar-element-api-base-*.js` 654 KB
  (lazy dependency of the bundle)
- per-element property-editor UI chunks load on demand when settings open

`npm run check:circular`, `npm run compile`, `npx wtr src/packages/tiptap`
all pass.

Related to #21152, builds on #22995.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Tiptap: Don't mount <umb-input-tiptap> in the standalone test

Mounting the element via fixture() spins up an UmbTiptapRteContext that
consumes UMB_SERVER_CONTEXT. In the unit-test runtime no server context
provider exists, so the context request stays pending. When @open-wc's
fixture tears down at end-of-file the request rejects with
"host disconnected" — surfaced as an unhandled promise rejection that
web-test-runner counts as a fatal runner error, exiting 1 even though every
individual test passed. The rejection happened to be in flight while a
block-grid clipboard test was active in CI, which is why the failure surfaced
there rather than in the tiptap test file itself.

Drop the manifest-registration assertion too — pulling the package-level
`manifests.ts` aggregator triggers a transitive 404 on the
`@umbraco-cms/backoffice/tiptap` importmap entry in the wtr environment.

The class-export + custom-element-registration checks are enough to prove
standalone exportability. The Storybook stories still cover the visual
end-to-end load path.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 14:32:20 +00:00
Mads RasmussenandJacob Overgaard f0013330e6 Backoffice: Embed package root manifests into umbraco-package.ts to reduce startup requests (#22957)
* Consolidate block package into index export

* keep umbraco-package.ts and embed manifests instead

* Inline package manifests into umbraco-package

* update docs
2026-05-27 14:26:56 +02:00
Mads RasmussenandJacob Overgaard bd2c985187 Backoffice: Embed implementations directly in core manifests to reduce startup network requests (#22944)
* Use direct imports in core manifests

* Extract theme aliases into constants file

---------

Co-authored-by: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com>
2026-05-27 14:26:34 +02:00
Mads RasmussenandJacob Overgaard 9711a5d012 Backoffice: Swap relative imports to @umbraco-cms/backoffice module imports in core packages (#22942)
Use @umbraco-cms/backoffice imports

Replace numerous relative/internal import paths with centralized '@umbraco-cms/backoffice' package entry points across core modules.This consolidates exports, simplifies import paths.
2026-05-27 14:24:57 +02:00
8160ede4b6 Background Jobs: Refine RecurringBackgroundJobBase API (#22966)
* Add IgnoredDelayChanged event to allow updates during back-off

* Make Period and IgnoredDelay settable on RecurringBackgroundJobBase with auto-raising events

* Address PR review: handle CTS race, restore negative-IgnoredDelay guard, clarify setter remarks

- Swallow ObjectDisposedException in OnIgnoredDelayChanged for the shutdown race where an in-flight handler reads the to-be-disposed CTS via Interlocked.Exchange before Dispose disposes it.
- Restore "skip back-off when IgnoredDelay <= TimeSpan.Zero (and not Timeout.InfiniteTimeSpan)" guard in IgnoreAndWaitAsync to defend against direct IRecurringBackgroundJob implementations / property overrides returning a negative value that would otherwise tight-loop via ComputeNextDelay clamping to zero.
- Add regression test for the negative-IgnoredDelay skip path.
- Mirror the constructor "stored without raising" remark on the Period and IgnoredDelay setter doc comments.

* Dispose newly-installed CTS when shutdown race wins the rotate-and-cancel

* Clarify XML docs.

* Introduce helper for cancellation source rotate and cancel.

---------

Co-authored-by: Andy Butland <abutland73@gmail.com>
2026-05-25 05:42:41 +00:00
Andy ButlandandSven Geusens 53c74efd35 Migrations: Append data-anchor value to href when missing in local link migration (closes #22860) (#22936)
* Support anchor fragments that are included in the data attribute but missing in the href when migrating local links.

* Addressed code review feedback.
2026-05-22 12:04:12 +02:00
Jacob OvergaardandClaude Opus 4.7 1ae2a780dd Workspace Actions: Restore waiting state for buttons with additional options (closes #18670, #20593) (#22554)
* Workspace Actions: Restore waiting state for buttons with additional options

The waiting state was suppressed whenever a workspace action reported
hasAdditionalOptions() (e.g. Save and publish on multi-variant sites),
so users saw the button jump straight from idle to the success tick
with no in-flight feedback.

Always set 'waiting' on click (unless the action is a link). The
variant-picker modal still opens on top of the button, so the spinner
is effectively invisible during selection — but it becomes visible
as soon as the modal closes and the publish request is in flight.

Fixes #22551

* Workspace Actions: Spin button only while real work is in flight

Replace the eager always-set-waiting behaviour from the previous commit
with an opt-in `isPending` signal so the spinner appears only while
actual work (validation + HTTP) is happening - not while the variant
picker modal is open, and never as a spurious success tick when the
user cancels the modal.

Changes:

- Add optional `isPending: Observable<boolean>` to UmbWorkspaceAction
  and a default UmbBooleanState + protected setPending() on the base
  class. Optional + backwards compatible for external implementers.

- Add optional `onActionStarting` callback (via a shared
  UmbWorkspaceActionExecutionOptions type) to
  UmbPublishableWorkspaceContext.saveAndPublish and
  UmbSaveableWorkspaceContext.requestSave. The document publishing
  context and content detail workspace base invoke the callback at the
  join point right after the variant picker resolves (or is skipped
  for the single-variant case), so it never fires when the modal is
  cancelled.

- Wire the document save and save-and-publish actions to clear pending
  at the start of execute() and pass an onActionStarting callback that
  flips it true when work begins.

- Update the workspace action element to observe api.isPending: when
  the observable is present the waiting state is driven by the
  observable (and the success tick is suppressed if the action
  resolves without ever signalling pending - i.e. a cancellation).
  When the observable is absent the element falls back to the legacy
  eager-waiting behaviour. Failures always surface the failed tick.

Fixes #22551

* Reduce cyclomatic complexity of #onClick and _handleSave

CodeScene Code Health Review flagged two complexity issues:

- UmbWorkspaceActionElement.#onClick reached cyclomatic complexity 9
  (threshold is < 9). Extracted the api-execution branch into a new
  private #runApiAction helper so #onClick collapses to a simple
  link-vs-action dispatch.
- _handleSave was already over the threshold (14); my optional-chain
  callback invocation pushed it to 16. Moved the
  `executionOptions?.onActionStarting?.()` call into a #notifyActionStarting
  helper so the call site is a plain method call and contributes zero
  cyclomatic complexity to _handleSave.

No behavioural change.

* Reduce cyclomatic complexity of #handleSaveAndPublish

Same fix as the previous commit's #notifyActionStarting extraction in
content-detail-workspace-base: move the optional-chain callback
invocation into a private helper so #handleSaveAndPublish stays at its
pre-PR cyclomatic complexity (15) instead of degrading to 17.

No behavioural change.

* DRY: extract notifyWorkspaceActionStarting into a shared utility

Both UmbDocumentPublishingWorkspaceContext.#handleSaveAndPublish and
UmbContentDetailWorkspaceContextBase._handleSave had identical private
optional-chain callback off the host method's cyclomatic complexity.

Replace both with a single exported notifyWorkspaceActionStarting()
utility co-located with UmbWorkspaceActionExecutionOptions. This:

- Removes a duplication point between the two contexts.
- Gives future workspace context implementations a ready-made way to
  honour the optional callback without re-inventing the helper or
  paying the cyclomatic-complexity cost at the call site.

No behavioural change.

* Rename isPending -> isExecuting to mirror the execute() method

Niels suggested correlating the observable's name with the action's
`execute()` method, so the symbol set is now:

- isExecuting (observable on UmbWorkspaceAction interface)
- _isExecuting / setExecuting (UmbWorkspaceActionBase)
- #observeIsExecuting / #executionStarted (workspace-action element)
- isExecutingObserver (observer alias)

Pure rename; no behavioural change.

* Address Copilot review: lazy isExecuting, observer scope, finally reset

Five Copilot findings on PR #22554. Three real regressions + two
contract violations, all addressed:

1. UmbWorkspaceActionBase always exposing `isExecuting` made every
   existing subclass appear to opt in to the new modal-aware flow,
   suppressing waiting/success states for actions that never call
   setExecuting(true). Made `_isExecuting`/`isExecuting` lazy: only
   created on the first setExecuting() call. Opt-in subclasses call
   `setExecuting(false)` in their constructor so the observable is
   exposed before the workspace-action element reads it. Subclasses
   that don't opt in keep `isExecuting` undefined and the element
   falls back to legacy eager waiting feedback.

2. Element observation of `isExecuting` now lives inside #runApiAction
   so it tracks whichever api is actually invoked (`_actionApi ?? #api`),
   correctly handling subclasses like UmbSaveAndPreviewWorkspaceActionElement
   that swap in a different api at runtime. The shared observer alias
   replaces any previous observation on re-clicks.

3. UmbSaveWorkspaceAction and UmbDocumentSaveAndPublishWorkspaceAction
   now wrap their execute() body in try/finally and reset
   setExecuting(false) on completion so the observable honours the
   "true while execute() is performing real work, false otherwise"
   contract instead of getting stuck at true between executions.

No behavioural change for actions that already worked correctly before
this PR; the regression-prone "always exposed" behaviour is gone.

* Address Claude review: Elements gap, type placement, tests + cleanup

Three follow-ups on top of c15eb2d0bc:

1. Elements gap — UmbElementSaveAndPublishWorkspaceAction +
   UmbElementPublishingWorkspaceContext now wire through the same
   onActionStarting/notifyWorkspaceActionStarting handshake as the
   Document equivalents, so multi-variant Elements (Forms, Commerce, etc.)
   get the spinner-after-modal behaviour rather than no spinner at all.

2. Type placement — moved UmbWorkspaceActionExecutionOptions out of
   publishable-workspace-context.interface.ts into its own file so the
   saveable interface no longer has a directional dependency on the
   publishable one. Both peer contexts now import from the same neutral
   location.

3. Unit tests — added blackbox coverage for notifyWorkspaceActionStarting
   (no-op on undefined options/callback, invokes when present) and the
   UmbWorkspaceActionBase.setExecuting lazy-opt-in contract (undefined
   until first call, observable then exposed, value flips, sequential
   emissions, stable reference across calls).

Code-review cleanup applied on the same pass:

- Dropped the redundant `setExecuting(false)` at the start of execute()
  in the save and save-and-publish actions; the finally block plus
  UmbBooleanState's value-dedup already cover idempotency on retries.
- Removed an overlong block comment on `_isExecuting`; the JSDoc on
  setExecuting already documents the lazy/opt-in contract for subclasses.
- Trimmed an internal motivation comment from notify-workspace-action-
  starting.function.ts that referenced cyclomatic complexity.
- Extracted a tiny makeAction() helper in the controller test to remove
  the `{ meta: {} as never }` repetition.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Workspace Actions: Align Save button state with Save & Publish (Andy review feedback)

Two related fixes addressing the variant-Save inconsistency Andy reported:

- Element catch block now only sets `failed` once `#executionStarted` is
  true. Pre-flight rejections (user cancelling a variant-picker modal,
  context-missing throws, etc.) leave the button idle, matching the
  silent-cancel path used by `#handleSaveAndPublish`. Legacy actions
  that don't opt in to `isExecuting` are unaffected because they set
  `#executionStarted = true` eagerly on click.

- `UmbDocumentWorkspaceContext._handleSave` and
  `UmbElementWorkspaceContext._handleSave` now accept and forward the
  `UmbWorkspaceActionExecutionOptions` argument to `super._handleSave`.
  The previous overrides dropped the parameter, so the
  `onActionStarting` callback supplied by `UmbSaveWorkspaceAction` never
  fired - which is why Save showed no waiting/success indicator even
  on a successful submit.

Result: Save and Save-and-publish now behave identically -
cancel = no indicator, submit = waiting then success - for both
invariant and multi-variant documents and elements.

* Docs: Document the modal-aware execution feedback contract for workspace actions

New 'Button state when the action opens a modal' subsection in
docs/workspaces.md explaining the three-piece contract:
UmbWorkspaceActionExecutionOptions + notifyWorkspaceActionStarting +
UmbWorkspaceActionBase.setExecuting. Covers third-party authoring of
modal-aware buttons, the cancel/pre-flight idle behaviour, and the
silent-parameter-drop pitfall on _handleSave overrides.

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 11:00:49 +02:00
Jacob OvergaardandClaude Opus 4.7 04f0e229c7 Backoffice: Coalesce small Rollup chunks across all workspaces (AB#67983) (#22896)
* Backoffice: Coalesce small Rollup chunks across all workspaces (AB#67983)

Set experimentalMinChunkSize=10_000 as the default in the shared Vite
helper. Every workspace inherits the coalescing automatically; the
threshold can still be overridden per workspace (pass 0 to disable).

Impact on dist-cms output:
- packages/core .js files: 981 -> 272 (-72%)
- All workspaces combined .js files: 2194 -> 1401 (-36%)
- Welcome dashboard .js requests: 510 -> 497 (-2.5%)
- packages/ufm requests in particular: 23 -> 12 (-48%)
- Gzipped bundle total: -1.2%
- Raw bytes: +1.4% (small overhead from merged chunks; gzip wins it back)

All entry chunks are preserved, so every public
@umbraco-cms/backoffice/<sub> import keeps resolving without changes
to package.json exports or tsconfig paths.

Further consolidation (collapsing core's per-subpath entries into a
single bundle with stubs) was prototyped but hits a TDZ cycle between
the eager entry and its dynamic-import descendants. Tracked for v18,
not part of this change.

* Backoffice: Normalise umbraco-package + manifests shapes (AB#67983)

Aligns the two outliers with the conventions used by the other 38
first-party packages:

- documents/umbraco-package.ts now uses the lazy bundle pattern
  (type: 'bundle', js: () => import('./manifests.js')) instead of
  eagerly importing manifests at module evaluation. The bundle
  initializer auto-loads the manifests at boot, so behaviour is
  unchanged.
- umbraco-news/manifests.ts now exports `manifests: Array<...>`
  instead of a bare `dashboard` object. The bundle initializer
  enumerates exports regardless of name, so behaviour is unchanged.

Preparatory cleanup so future build-time manifest aggregation can
treat every workspace uniformly.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 09:35:44 +02:00
65ab1c0b2b Background Jobs: Rewrite RecurringHostedServiceBase with SemaphoreSlim and add signalling support (#22331)
* Compute next delay to compensate for time drift

* Use SemaphoreSlim to properly handle exceptions, cancellation tokens and triggering immediate executions

* Add RecurringBackgroundJobBase to contain default values and hide obsoleted method

* Add NextExecutionStrategy parameter to adjust the schedule after triggered executions

* Add TriggerExecution methods to RecurringBackgroundJobHostedServiceRunner

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Handle cancellation (application shutdown) and publish RecurringBackgroundJobCanceledNotification

* Match hosted services by Type instead of type name string

* Extract shared helper for TriggerExecution tests

* Clear trigger state when initial delay is interrupted

* Clear _nextExecutionSkipOnOvershoot unconditionally

* Combine ComputeNextDelay tests

* Consolidate trigger state into an immutable record for thread safety

* Use ConcurrentDictionary for thread-safe hosted service lookup

* Remove hosted services from dictionary on stop

* Fix API compatibility errors

* Removed unneeded using.

* Register RecurringBackgroundJobHostedServiceRunner as resolvable singleton

* Remove failed hosted service from dictionary when StartAsync throws

* Use semaphore signaling instead of Task.Delay in trigger tests

Use semaphore signaling instead of Task.Delay in trigger tests 2

* Inject TimeProvider into RecurringHostedServiceBase for deterministic testing

Fix timeprovider

* Use DelayCalculator.GetDelay instead of RecurringHostedServiceBase.GetDelay

* Fix Exception_In_PerformExecuteAsync_Does_Not_Kill_Loop test

* Avoid disposing period-change CTS while wait loop may still reference it

* Configure IEventMessagesFactory mock to return real EventMessages

* Clarify TriggerExecution(TimeSpan) docs and add ChangePeriod test

* Validate period is positive and use GetOrAdd to avoid creating unused hosted services

* Set up Period and Delay on mock job to satisfy constructor validation

* Ensure PeriodChanged event is unsubscribed again

* Fix trigger state race, simplify ReleaseSignal, and add canceled notification test

Fix trigger state

* Use Interlocked for _period reads/writes and implement thread-safe dispose pattern

* Remove hosted service from dictionary before stopping to prevent triggering during shutdown

* Replace Task.Yield with semaphore timeouts in negative assertions

* Tidy RecurringBackgroundJobBase docs and runner error handling

* Wait IgnoredDelay after ignored execution to prevent tight looping when Period is short or zero

* Add IRecurringBackgroundJobTrigger<TJob> for opt-in job triggering

* Register IRecurringBackgroundJobTrigger as open generic and drop AddTriggerableRecurringBackgroundJob

* Fix and add parameter validation

* Allow Timeout.InfiniteTimeSpan as Period for manual-trigger-only recurring jobs

* Migrate built-in jobs to RecurringBackgroundJobBase and require ITriggerableRecurringBackgroundJob in runner trigger overloads

* Support infinite Delay and honor TriggerExecution(TimeSpan) issued during the initial delay

* Handle edge case of backoff via InfiniteTimeSpan.

* Refactored large method.

* Added clarifying documentation.

* Suppress ExecutionContext flow when starting the recurring background loop, restoring previous timer behaviour.

* Relocate Suppress ExecutionContext flow to avoid package validation error.

* Align IRecurringBackgroundJobTrigger generic type constraint with AddRecurringBackgroundJob

* Rename ApplyTriggerState to ComputeNextDelayFromTriggerState

* Allow Timeout.InfiniteTimeSpan as IgnoredDelay to fully disable a job for the remaining application lifecycle

* Fix generic type constraint

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Andy Butland <abutland73@gmail.com>
2026-05-21 19:39:21 +02:00
Andreas Lykke BorgandAndy Butland 1616997409 Image Cropper: Improve contrast of append label in crop options editor (closes #22878) (#22917)
Improve contrast of input append label in image crops editor
2026-05-21 19:06:01 +02:00
Engiber LozadaandAndy Butland 63289e22cb Block Grid: Fix inline create button width not updating on workspace resize (closes #22527) (#22928)
* Add ResizeObserver for inline create buttons

* Disconnect layout resize observer on destroy

* Initialize ResizeObserver and simplify cleanup

* Remove optional chaining on layout observer disconnect
2026-05-21 18:12:23 +02:00
Andy ButlandandLan Nguyen Thuy 7f832d261d Reset password: Add inline validation messaging for password pattern requirements (#22880)
* add custom validation for password input in reset password

* update remove invalid listeners in disconnectedCallback

---------

Co-authored-by: Lan Nguyen Thuy <lnt@umbraco.dk>
2026-05-21 17:34:05 +02:00
Mads RasmussenandGitHub f79e9586b4 Collections: Replace direct filter pass-through in collection server data sources (#22921)
Pass explicit skip/take to collection services
2026-05-21 09:18:36 +01:00
Mads RasmussenandGitHub c74a58246f Entity Data Picker: Fix "Not Found" in remove dialog for entities without a top-level name (#22915)
* Add item data resolver support to picker data sources

* add js docs

* remove duplicated fallback logic

* wip unit tests of requestItemName method

* Use DocumentVariantStateModel in mock documents to fix compiler

* Update input-entity-data.context.ts

* Update input-entity-data.context.test.ts
2026-05-21 09:13:45 +01:00
nikolajlauridsen 06b15157cf Merge branch 'release/17.4.2' into release/17.5.0
# Conflicts:
#	src/Umbraco.Web.UI.Client/package-lock.json
#	src/Umbraco.Web.UI.Client/package.json
#	tests/Umbraco.Tests.AcceptanceTest/package-lock.json
#	tests/Umbraco.Tests.AcceptanceTest/package.json
#	version.json
2026-05-21 09:18:11 +02:00
MoleandGitHub b87d519bf2 Cache: Only write to url table on a single server in load balanced environments to remove lock contention (#22890)
* Move database writes out of cache refreshers

* add tests

* Fix up tests
2026-05-20 17:08:33 +02:00
8aaac65f83 Content Editing: Fix save composition values on invariant content and save of default segment (closes #22800, #22865) (#22846)
* Fix edit of a variant property composed to an invariant document.

* Collapse multi-line guard comment to a single line per project policy.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Add unit test coverage for invariant content with a culture-variant composition property.

Adds a third mock document/document-type pair representing an invariant
content type whose flattened property list contains a culture-variant
property (the runtime shape produced when a variant composition is applied
to an invariant content type) and a setPropertyValue test asserting the
value is stored as a culture/segment-invariant entry.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Remove null guard for segment variant documents, as null segment is the default segment.

* update mock data and tests to include real compositions

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: Mads Rasmussen <madsr@hey.com>
2026-05-20 10:52:09 +02:00
Andy Butlandandmole f255fd7bff Bump version to 17.4.2. 2026-05-20 09:18:56 +02:00
7527de7c56 Migrations: Add auto upgrade coordination for load-balanced setups (#22815)
* Add auto upgrade coordination for load balanced setups

* Add tests

* Apply suggestions from code review

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* fix(infrastructure): move TryBecomeLeaderAsync inside try/catch in UnattendedUpgradeBackgroundService

Ensures DB exceptions thrown during migration coordination set BootFailed
rather than faulting the background service silently.

* Fix feedback

* Update src/Umbraco.Infrastructure/Install/MigrationCoordinator.cs

Co-authored-by: Andy Butland <abutland73@gmail.com>

* Recheck state

* fix(tests): update concurrent race test for post-claim DetermineRuntimeLevel check

The winner now calls DetermineRuntimeLevel() once from the post-claim check
and must see Upgrading; the loser polls twice before seeing Run. Transition
the mock on the second call instead of the first.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Andy Butland <abutland73@gmail.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-20 09:18:56 +02:00
df12a3e467 Cache: Add scope-level cache version tier to reduce DB hits in bulk operations (#22563)
* Cache cacheversion on scope

* Add tests

* Cache: Use ConcurrentDictionary for the inner per-scope version map

The inner Dictionary<string, Guid> was not thread-safe. Replacing it
with ConcurrentDictionary<string, Guid> removes the hidden assumption
that the root scope is only accessed from a single thread at a time.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* Update src/Umbraco.Core/Cache/IRepositoryCacheVersionAccessor.cs

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update tests/Umbraco.Tests.UnitTests/Umbraco.Web.Common/Cache/RepositoryCacheVersionAccessorTests.cs

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update src/Umbraco.Web.Common/Cache/RepositoryCacheVersionAccessor.cs

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Skips failing test so we can run nightly. THIS NEEDS TO BE REVERTED

* Revert "Skips failing test so we can run nightly. THIS NEEDS TO BE REVERTED"

This reverts commit c34d1736c336b3fcf7803b44e88f6018fa45c275.

* Only write version once pr. scope

* Add tests

* Remove unnececary locks

* Fix thread-safety: replace HashSet with ConcurrentHashSet and use GetOrAdd to eliminate TOCTOU races

* Add unit tests for RepositoryCacheVersionService

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Andreas Zerbst <andr317c@live.dk>
2026-05-20 09:18:56 +02:00
Andy Butland 0b86312f52 Children/Descendants: improve traversal performance (closes #22646) (#22742)
* Add benchmark test for measuring improvements to children and descendant retrieval.

* Remove unnecessary sort from retrieval of children.

* Return the result of the filtered collection of children/decendants without materialising.

* Lazily build property wrappers when materializing IPublishedContent.

* Cache the ordered children list on NavigationNode.

* Cache descendants per parent on the navigation snapshot.

* Add synchronous fast path for retrieved of cached content.

* Additional unit tests.

* Add TODO to make UpdateSortOrder internal.

* Addressed code review feedback.

* Further unit tests.

* Future-proofed code comments.
2026-05-19 18:01:12 +02:00
Andy Butland 4c909d8ce8 Merge branch 'release/17.4.1' into release/17.5.0 2026-05-19 17:54:19 +02:00
Niels LyngsøandGitHub ba29b91301 Slider: fix duplicated property editor settings properties (#22898)
* fix duplicate slider pe-settings properties

* remove comment

* avoid throws
2026-05-19 14:19:34 +00:00
Andy ButlandandGitHub 336bffe4c4 Output Caching: Correctly gate auto-registration of UseOutputCache() middleware (#22897)
Correct the gating of the call to UseOutputCache() to only proceed Umbraco managed caching via configuration is enabled, and not consider existing implementation specific registrations.
2026-05-19 16:18:21 +02:00
426e516c61 Content Workspace: Load Data-Types based on Loaded Content Types (#22886)
* Load Data-Types based on Loaded Content Types

* Update Comment

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* mergeObservables approach

---------

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-05-19 15:02:13 +02:00
Andy Butland c718a3ce12 Bump version to 17.4.1. 2026-05-19 15:01:44 +02:00
Jacob Overgaard 7737cd3d40 Localization: Honor DefaultUILanguage on initial load (closes #22808) (#22822)
* Localization: Honor DefaultUILanguage on initial load (closes #22808)

Closes #22808.

Previously, the configured DefaultUILanguage was silently overridden to
'en' at startup because the UmbLocalizationRegistry constructor called
loadLanguage(UMB_DEFAULT_LOCALIZATION_CULTURE) unconditionally. The
configured locale rendered into <html lang="..."> by Razor never had a
chance to flow through to the active language.

Changes:

- localization.registry.ts: stop forcing the active language to 'en'
  in the constructor. Initial state is canonicalised from
  document.documentElement.lang, falling back to 'en' for empty or
  malformed input. The extension filter now always includes the
  default culture alongside the active locale so 'en' translations
  remain available as a key-level fallback regardless of which
  language is active. A synchronous tap mirrors the active locale to
  document.lang and the manager when the state changes, so a fresh
  element rendered between loadLanguage() and the async translation
  load picks up the right language immediately.

- localization.manager.ts: drop the MutationObserver on
  document.documentElement and rely on the registry as the single
  channel for language changes. setActiveLanguage accepts a `silent`
  option so the synchronous tap can update fields without firing a
  consumer notification (translations may still be loading). A new
  notifyLanguageChanged() method is fired by the registry once
  translations are in place.

- app.element.ts: subscribe to umbLocalizationRegistry.currentLanguage
  in connectedCallback and mirror it onto the host element's lang
  attribute, so myApp.lang reflects the source of truth rather than a
  stale snapshot of <html lang>.

- auth.element.ts (login app): same lang subscription, plus after the
  slim backoffice controller registers extensions, prefer the
  visitor's navigator.language if a matching localization extension
  exists (falls through baseName -> language -> en automatically).

Tests: new initialization tests for the registry, manager
setActiveLanguage tests, and the controller tests refactored to use
the new explicit setActiveLanguage API instead of writing directly to
document.documentElement.lang.

* Login: Only override DefaultUILanguage with navigator.language when default has no translation

If the admin sets DefaultUILanguage to a language we have a translation for,
respect that choice over the visitor's browser language. Falling back to
navigator.language only when the configured default isn't available avoids
silently ignoring the admin's explicit setting (e.g., DefaultUILanguage='da-DK'
on a site whose visitor's browser is 'en-GB' should still show Danish).

* Simplify: split setActiveLanguage from notifyLanguageChanged

Drop the silent option in favor of two intent-revealing methods:
setActiveLanguage updates the active language and direction without
side-effects; notifyLanguageChanged tells all connected controllers
to re-render against the current state. Callers compose them based
on what they need (the registry's pipeline updates language sync
then flushes notifications after async translation load).

Also extracts baseLocaleOf() helper, simplifies the navigator.language
match logic in the login app's #applyPreferredLanguage, and removes
narration-style comments in the new code.

* Restore deprecated UmbLocalizationManager.updateAll for backward compat

The old MutationObserver-driven updateAll() field was technically part
of the manager's public surface. Restore it as a deprecated alias that
reads document.lang/dir and forwards to setActiveLanguage + notifyLanguageChanged,
with a runtime UmbDeprecation warning pointing consumers at the new API.

* Fix deprecation removal version to v20 + correct baseLocaleOf JSDoc

Per the deprecation policy in CLAUDE.md (current major + 2): a method
deprecated in v18 must remain through v19 before removal, so the
earliest removal is v20, not v19.

Also corrects the baseLocaleOf JSDoc — Intl.Locale.baseName can include
script subtags (e.g. 'zh-Hant-TW'), not just language and region.

* Scope the active language to the host element, drop navigator.language

- Razor now sets `lang` on `<umb-app>` and `<umb-auth>` from
  DefaultUILanguage. The element passes its lang through on connect, so
  the host owns its own scope — future multi-backoffice scenarios (e.g.
  signing into two Umbraco Cloud sites in the same document) get their
  own language without fighting over a global `<html lang>`.

- The registry no longer reads or writes `document.documentElement.lang`.
  Host elements drive it via `loadLanguage()`; `<html lang>` stays as
  whatever Razor rendered.

- Removed the navigator.language preference detection in the login app.
  Not in scope for the bug fix and adds behavior the admin can't opt out
  of. The existing current-user-locale flow already handles per-user
  preference after login.

- Tests updated to assert on `umbLocalizationManager.documentLanguage`
  instead of `document.documentElement.lang`.

* Set <html lang="en"> to match the static (noscript) text in the templates

The page's `<html lang>` should describe the language of the document's
own innate content. Both Index.cshtml files only contain English static
text (the noscript fallback), so the page-level lang is now "en".

The dynamic UI inside <umb-app> / <umb-auth> carries its own `lang`
attribute (from DefaultUILanguage), which overrides for that subtree —
correct per the HTML spec for language inheritance.

* Drop deprecated UmbLocalizationManager.updateAll

It was public as an artifact of being an arrow function so it could be
passed to a MutationObserver without binding — not because it was
intended as part of the public API. External usage is effectively
zero, and the new explicit setActiveLanguage + notifyLanguageChanged
covers anyone who did reach for it.

* Docs: document active-language-on-host pattern in package-development.md

After PR #22822, the active UI language is driven by the shell elements
(<umb-app>, <umb-auth>) via their own lang attribute, not by <html lang>.
Document that so future contributors don't reach for the global.

* Collapse setActiveLanguage + notifyLanguageChanged into one method

The silent-write path is just `manager.documentLanguage = ...` — no new
method needed; the field is already public and was always writable. The
notify path keeps setActiveLanguage, which now both sets and notifies.

Net: one new public method on the manager instead of two.

* Inline the active-language write in the registry, drop setActiveLanguage

The previous version added setActiveLanguage on the manager as
a 'cleaner API' than direct field writes. But the manager's fields
(documentLanguage, documentDirection, connectedControllers) have
always been public, the registry is the only caller, and the wrapper
was just more public surface to maintain through the eventual
manager/registry collapse.

Net: -70 lines across the test file, no new public methods on the
manager, the registry pipeline writes the fields and iterates the
controllers directly where it would have called setActiveLanguage.

* Document that documentLanguage/Direction are read-only for consumers

Note in JSDoc that the only supported way to change the active language
is umbLocalizationRegistry.loadLanguage(). The fields stay writable for
the registry pipeline (cross-module internal); the comment is here so
the next contributor doesn't reach for them as a shortcut and end up
with the manager state out of sync with what's actually loaded.
2026-05-19 10:52:55 +02:00
139ac6ad72 Blueprints: Fix UdiEntityTypeHelper.ToUmbracoObjectType() for document blueprint containers (#22875)
* Blueprints: Fix UdiEntityTypeHelper.ToUmbracoObjectType() for document blueprint containers

Adds the missing DocumentBlueprintContainer case so the conversion is
symmetric with FromUmbracoObjectType().

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Add missing case for MemberTypeContainer.

* Use reflection to ensure other future missed cases are surfaced without having to explicitly extend the tests.

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: Andy Butland <abutland73@gmail.com>
2026-05-19 09:13:52 +02:00
Andy Butland f964a18b5b Merge branch 'release/17.5.0' of https://github.com/umbraco/Umbraco-CMS into release/17.5.0 2026-05-14 19:10:30 +02:00
Lee KelleherandAndy Butland 2369f00544 Mocks: Add missing signalR property to mock server configuration response (#22849)
Mocks: Add missing signalR property to mock server configuration response

The GetServerConfigurationResponse type was updated in #22700 to require
a signalR.skipNegotiation property, but the MSW mock handler was not
updated to match, causing a tsc compilation error.
2026-05-14 19:08:11 +02:00
Sebastiaan Janssen d822518dd3 Core: Preserve path case in ShadowFileSystem (#22838)
* Core: Preserve path case in ShadowFileSystem

ShadowFileSystem stored staged files at their original case via _sfs.AddFile
but tracked them under a lowercased key (NormPath calling ToLowerInvariant).
On Complete(), Inner.AddFile(kvp.Key, _sfs.GetFullPath(kvp.Key)) reconstructed
the staged file's path from the lowercased key, so on case-sensitive file
systems (Linux) File.Move failed with FileNotFoundException whenever a path
contained any uppercase character.

Drop the ToLowerInvariant from NormPath and switch the tracking dictionary
to StringComparer.OrdinalIgnoreCase. Lookups remain case-insensitive
(matching Windows semantics) while the stored key now matches what was
written to disk. IsChild/IsDescendant updated to OrdinalIgnoreCase
StartsWith for consistency.

Added regression test reproducing the original FileNotFoundException with
Views/PageNotFound.cshtml.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Rename regression test to follow Can_ naming convention

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Track canonical staged path per shadow node

The case-insensitive node dictionary preserved only the first inserted
key, so re-staging a logical path with a different case (e.g. AddFile
"Views/Foo.cshtml" then "views/foo.cshtml") wrote a phantom second file
to _sfs on Linux while Complete still resolved the original key — leaving
orphaned shadow files and committing stale content.

Track the original-case staged path on each ShadowNode and route all
_sfs operations (AddFile, OpenFile, GetFullPath, GetLastModified,
GetCreated, GetSize, MoveFile, Complete) through that canonical path.
Inner.AddFile on commit still uses the stored dictionary key, so the
destination case in the inner file system is unchanged.

Expanded the regression test to also exercise OpenFile, GetSize and
AddFile against a different-cased path, and to assert that the staged
file is written exactly once.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Make ShadowNode.CanonicalPath non-nullable

Every node now carries the original-case path it tracks, set at construction.
This removes the defensive 'sf.CanonicalPath ?? path' fallbacks at the read
sites (OpenFile, GetFullPath, GetLastModified, GetCreated, GetSize, Complete)
which were unreachable but noise.

The GetCanonicalPath helper is gone; AddFile and MoveFile now use the existing
node variable inline ('sf?.CanonicalPath ?? path' — node can legitimately be
null when staging a path for the first time).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Address Copilot review: normalize delete key, cross-platform test

DeleteDirectory(recursive=false) stored the deletion marker under the
caller-supplied path (which can contain backslashes) instead of the
normalized key, so a follow-up NormPath-based lookup could miss the
deletion and IsChild scans could become inconsistent. Use normPath.

The shadow-second-file assertion in the regression test used
File.Exists on a different-cased path; that returns true on
case-insensitive file systems (Windows / default macOS) regardless of
the actual stored case, so the assertion was platform-dependent.
Replaced it with a directory-count check that's cross-platform.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Add cross-platform regression test so any reversion would be caught on a non-case sensitive file system.

* Cleaned up warnings, obsoletions and comments in the existing tests.

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: Andy Butland <abutland73@gmail.com>
(cherry picked from commit abfa8cb144)
2026-05-14 10:42:20 +02:00
Andy Butland 21ab470d88 Merge branch 'release/17.4.0' into release/17.5.0 2026-05-14 08:31:39 +02:00
Andy Butland 3aa87fec96 Bump version to 17.4.0. 2026-05-13 17:39:57 +02:00
Ronald BarendseandAndy Butland 4921ab9257 SignalR: Mark ServerEventSender as a distributed cache notification handler (#22818)
* Mark ServerEventSender as distributed cache notification handler

* Batch and deduplicate notifications in ServerEventSender

* Introduce IDistributedCacheAsyncNotificationHandler<T> and use it in ServerEventSender

* Add ServerEventSender unit tests and address PR review feedback
2026-05-13 08:29:22 +02:00
Ronald Barendseandleekelleher 63bae5958a User Permission: Re-export fallback condition config type and global augmentation (#22794)
(cherry picked from commit 55fec1dc2a)
2026-05-12 17:15:28 +01:00
Andy Butlandandleekelleher 35d726ad31 Sort Children: Show loading state on Sort button (closes #22651) (#22813)
* Add submit button state to sort dialog.

* Guard against re-entrant submit in sort-children-of modal.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Set failed button state when sort-children-of submit throws.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
(cherry picked from commit dfe93c5639)
2026-05-12 17:06:44 +01:00
Kenn JacobsenandAndy Butland fc9ca861b0 Content: Ensure correct variant change tracking when unpublishing variant content (#22799)
* Ensure correct change tracking when unpublishing

* Update src/Umbraco.Core/Services/PublishStatus/PublishStatusService.cs

Co-authored-by: Andy Butland <abutland73@gmail.com>

* Add comment

---------

Co-authored-by: Andy Butland <abutland73@gmail.com>
2026-05-12 10:01:20 +02:00
Sven Geusensandmole 68194e1a27 Distributed background jobs: Improve gracefull shutdown behaviour (#22796)
* Dont fail silently on missing ambientscope

This makes it in line with other methods in the repo

* Pass on Cancellationtoken to the job to support gracefull job shutdown

(cherry picked from commit 5ae17ace6a)
2026-05-12 09:51:25 +02:00
1c1787c445 Auth: Un-deprecate getLatestToken and route per-request fetches through it (#22736)
* Auth: un-deprecates getLatestToken and routes per-request fetches through it

getLatestToken is the only public API for "wait for any in-flight refresh,
trigger one if the access token has expired, then return". External and
internal consumers were warned off it without an equivalent replacement:
configureClient only helps @hey-api/openapi-ts clients, and consumers using
axios/ky/native fetch had no other gate.

- Removes the @deprecated JSDoc + UmbDeprecation.warn() call so the public
  surface no longer prints a console warning per call.
- Uses getLatestToken.bind(this) for the auth callback inside configureClient
  and the token callback inside getOpenApiConfiguration so both paths share
  the same #ensureTokenReady gate.
- Replaces the hard-coded `Authorization: Bearer [redacted]` in unlinkLogin
  and #makeLinkTokenRequest with `Bearer ${await getLatestToken()}` so those
  fetches participate in the refresh coordination rather than firing with a
  potentially-revoked cookie.

Also wires the UmbracoExtension template's entrypoint to call
authContext.configureClient(client), matching the v18 template change. The
framework awaits onInit, so this guarantees the API client is fully
configured before any element in the extension can use it.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Auth: tightens UmbAuthContext correctness and accepts any hey-api client

Pulls in a batch of non-breaking improvements to UmbAuthContext that came
out of an audit on the back of the un-deprecation work in this PR:

Public surface:
- configureClient(client) now accepts a new structural UmbApiClient type
  (exported from @umbraco-cms/backoffice/http-client). Each @hey-api/openapi-ts
  generation produces a fully-bound Client<…>; the backoffice's umbHttpClient
  and an extension's regenerated client are structurally identical but TS
  treats them as distinct generic instantiations. The widened parameter lets
  extensions wire their own client without `as never` casts at call sites.
  bindDefaultInterceptors keeps its strict typeof umbHttpClient parameter
  (preserving autocomplete inside interceptor callbacks); the cast happens
  once, internally.

Correctness:
- The auth context now holds a single UmbApiInterceptorController, lazy-
  initialised on first configureClient() call. Previously each call
  instantiated a new controller, which re-provided the UmbAuthSignalerContext
  on the host and stacked listeners — visible the moment an extension also
  called configureClient. One controller for the lifetime of the host, all
  configured clients share it.
- completeAuthorizationRequest checks sessionStorage before asking
  window.opener for the PKCE verifier. The previous order hung for the full
  postMessage timeout whenever oauth_complete loaded with a non-OAuth
  window.opener (which is set for ANY window.open target). The opener
  postMessage timeout is also dropped from 5s to 1.5s — a real popup parent
  responds within milliseconds; longer is just wait time for the unrelated-
  opener case.
- The cross-tab 'authorized' BroadcastChannel handler now routes through
  #setSessionLocally so the timestamp math stays in one place. The
  'sessionUpdate' handler still applies pre-computed timestamps directly
  (peer broadcast already did the math) but does so inside the
  #inSessionUpdateCallback guard, so a synchronous session$ observer can no
  longer trigger a spurious /token refresh on top of a peer's update.
- #ensureTokenReady drops its query-then-request pattern. Now always queues
  behind the umb:token-refresh lock with a no-op callback — if the lock is
  free it acquires immediately, if held it waits. Eliminates the race window
  between query() and request().
- destroy() invokes #popupCleanup before tearing down so an in-flight popup
  flow's window-level message listener and closed-poll interval don't leak
  past the context's lifetime. The cleanup helper itself now resolves the
  popup-flow Promise — every termination path (authorized, popup closed,
  superseded by a new flow, context destroyed) is observable to the awaiter
  instead of hanging forever.

Cleanup:
- makeAuthorizationRequest is annotated Promise<void> so the redirect and
  popup branches share an explicit return type.
- unlinkLogin wraps the parsed problem-details payload in a real Error (with
  the original payload exposed on `.cause`) so callers using `instanceof
  Error` or expecting a stack trace get sane behaviour.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 10:58:19 +02:00
6c5873047b Auth: Un-deprecate getLatestToken and route per-request fetches through it (#22736)
* Auth: un-deprecates getLatestToken and routes per-request fetches through it

getLatestToken is the only public API for "wait for any in-flight refresh,
trigger one if the access token has expired, then return". External and
internal consumers were warned off it without an equivalent replacement:
configureClient only helps @hey-api/openapi-ts clients, and consumers using
axios/ky/native fetch had no other gate.

- Removes the @deprecated JSDoc + UmbDeprecation.warn() call so the public
  surface no longer prints a console warning per call.
- Uses getLatestToken.bind(this) for the auth callback inside configureClient
  and the token callback inside getOpenApiConfiguration so both paths share
  the same #ensureTokenReady gate.
- Replaces the hard-coded `Authorization: Bearer [redacted]` in unlinkLogin
  and #makeLinkTokenRequest with `Bearer ${await getLatestToken()}` so those
  fetches participate in the refresh coordination rather than firing with a
  potentially-revoked cookie.

Also wires the UmbracoExtension template's entrypoint to call
authContext.configureClient(client), matching the v18 template change. The
framework awaits onInit, so this guarantees the API client is fully
configured before any element in the extension can use it.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Auth: tightens UmbAuthContext correctness and accepts any hey-api client

Pulls in a batch of non-breaking improvements to UmbAuthContext that came
out of an audit on the back of the un-deprecation work in this PR:

Public surface:
- configureClient(client) now accepts a new structural UmbApiClient type
  (exported from @umbraco-cms/backoffice/http-client). Each @hey-api/openapi-ts
  generation produces a fully-bound Client<…>; the backoffice's umbHttpClient
  and an extension's regenerated client are structurally identical but TS
  treats them as distinct generic instantiations. The widened parameter lets
  extensions wire their own client without `as never` casts at call sites.
  bindDefaultInterceptors keeps its strict typeof umbHttpClient parameter
  (preserving autocomplete inside interceptor callbacks); the cast happens
  once, internally.

Correctness:
- The auth context now holds a single UmbApiInterceptorController, lazy-
  initialised on first configureClient() call. Previously each call
  instantiated a new controller, which re-provided the UmbAuthSignalerContext
  on the host and stacked listeners — visible the moment an extension also
  called configureClient. One controller for the lifetime of the host, all
  configured clients share it.
- completeAuthorizationRequest checks sessionStorage before asking
  window.opener for the PKCE verifier. The previous order hung for the full
  postMessage timeout whenever oauth_complete loaded with a non-OAuth
  window.opener (which is set for ANY window.open target). The opener
  postMessage timeout is also dropped from 5s to 1.5s — a real popup parent
  responds within milliseconds; longer is just wait time for the unrelated-
  opener case.
- The cross-tab 'authorized' BroadcastChannel handler now routes through
  #setSessionLocally so the timestamp math stays in one place. The
  'sessionUpdate' handler still applies pre-computed timestamps directly
  (peer broadcast already did the math) but does so inside the
  #inSessionUpdateCallback guard, so a synchronous session$ observer can no
  longer trigger a spurious /token refresh on top of a peer's update.
- #ensureTokenReady drops its query-then-request pattern. Now always queues
  behind the umb:token-refresh lock with a no-op callback — if the lock is
  free it acquires immediately, if held it waits. Eliminates the race window
  between query() and request().
- destroy() invokes #popupCleanup before tearing down so an in-flight popup
  flow's window-level message listener and closed-poll interval don't leak
  past the context's lifetime. The cleanup helper itself now resolves the
  popup-flow Promise — every termination path (authorized, popup closed,
  superseded by a new flow, context destroyed) is observable to the awaiter
  instead of hanging forever.

Cleanup:
- makeAuthorizationRequest is annotated Promise<void> so the redirect and
  popup branches share an explicit return type.
- unlinkLogin wraps the parsed problem-details payload in a real Error (with
  the original payload exposed on `.cause`) so callers using `instanceof
  Error` or expecting a stack trace get sane behaviour.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 10:57:32 +02:00
Andy Butland 3714ebbb29 Published Content: Fix Fallback.ToAncestors with no match throwing exception at property level (closes #22759) (#22763)
* Fix Fallback.ToAncestors regression at property level.

* Further unit tests.
2026-05-08 10:18:09 +02:00
Andy Butland 58b047bf7e Published Content: Fix Fallback.ToAncestors with no match throwing exception at property level (closes #22759) (#22763)
* Fix Fallback.ToAncestors regression at property level.

* Further unit tests.
2026-05-08 10:17:20 +02:00
Niels Lyngsø bf32f9e5a6 update package-lock 2026-05-08 09:01:20 +02:00
Niels Lyngsø 3220739faa upgrade to UI LIbrary 1.17.3 2026-05-08 08:59:49 +02:00
Jacob OvergaardandGitHub ae4ac2a4b9 build(deps): bumps @umbraco-ui/uui to 1.17.3 (#22753) 2026-05-08 08:52:11 +02:00
Andreas ZerbstandGitHub 54ded689e8 E2E: QA: Add .prettierrc.json to acceptance tests for formatting consistency (#22751)
Add .prettierrc.json to acceptance tests for formatting consistency
2026-05-08 09:19:26 +07:00
Andy ButlandandJacob Overgaard 17e73eee28 Color Picker: Refresh stored label when data type label changes (closes #22741) (#22761)
* Update stored color label if changed on save of document with color picker.

* Clarify intent of change event dispatch in label sync

* Make comparison case insensitive.

* Added unit tests for new behaviour.
2026-05-07 21:14:52 +02:00
Andy ButlandandGitHub 2292b7479d Color Picker: Refresh stored label when data type label changes (closes #22741) (#22761)
* Update stored color label if changed on save of document with color picker.

* Clarify intent of change event dispatch in label sync

* Make comparison case insensitive.

* Added unit tests for new behaviour.
2026-05-07 21:13:05 +02:00
Andy ButlandandGitHub 9b1fc50de3 Dictionary: Order SQL before FetchOneToMany to prevent duplicate items in collection view (closes #22640) (#22750)
* Order SQL before FetchOneToMany in dictionary entry retrieval to prevent duplicate items in collection view.

* Used PrimaryKey instead of UniqueId to take advantage of the clustered index.
2026-05-07 14:29:43 +00:00
3052b57203 E2E: QA: add acceptance tests for content versioning (#22702)
* Added tests

* Updated

* Cleaned up

* Fixes based on comments

* Apply suggestions from code review

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* Added helpers for verifying document

* Removed redundant method

* Cleaned up

* Reverted deletion of constants

* Undo revert

* Fixes based on comments

* updated command

* Added removed method

* Update smokeTest command in package.json

---------

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-05-07 10:13:29 +00:00
aba8e3eb7a Icons: developer icon manager (#22437)
* extend icons with information from theseaurus

* implement new icon search logic

* clean-up data

* icon manager

* sorting with a backup of the name

* refactor into a controller

* improve multi word group search

* embed lucide data

* rename tech into technology

* remove paper from dollar

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* improve search

* related should not show up in search

* update threshold

* separate name words

* also consider full icon name match

* better comment

* other approach for full name matches

* full icon name search if query contains a -

* fix test

* remove related code

* updates to related

* make its own package

* revert changes

* update tsconfig

* package-lock

---------

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-05-07 10:11:05 +00:00
Mads RasmussenandGitHub 040a0d5e49 Document Workspace: Add CRUD and property value tests for document workspace context (#22621)
* temp mock set

* test getPropertyValue

* Extend document workspace context tests to cover read/write property values

* move context files into context folder

* Add document CRUD tests, mock handler & interceptor

* temp mock error interceptor

* Return 404 when document not found

* Use undefined for entity unique state until initialized

* Fix import paths for document workspace editor

* Add test utils and extend document workspace tests

* Update document-workspace-context.test-utils.ts

* Match invariant variant when variantId missing

* Ensure finishPropertyValueChange runs on exit

Wrap setPropertyValue implementation in a try/finally and move finishPropertyValueChange into the finally block so cleanup always runs even if an error is thrown. No other functional changes — code was re-indented and organized but behavior remains the same except for guaranteed cleanup on error.

* Require variantId for culture/segment-variant props

* fix types

* fix mock modal typescript error

* Distinguish unloaded vs root entity unique

* use the real current user context

* hide mock set in UI

* rename mock set

* Move initiatePropertyValueChange into try

* Use 'satisfies' for UmbMockDataSet assertions

* Preserve requested unique on failed load

* Treat missing variantId as invariant

* Reset update lock on destroy

* remove unused group + user

* Guard _current.unmute and remove destroy override

* Add tests for element data manager

* Guard subject access and add destroy test

* Throw when calling methods after destroy
2026-05-07 09:36:05 +02:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>Jacob Overgaard
ef01edb46a Bump lodash from 4.17.21 to 4.18.1 in /src/Umbraco.Web.UI.Client (#22723)
Bumps [lodash](https://github.com/lodash/lodash) from 4.17.21 to 4.18.1.
- [Release notes](https://github.com/lodash/lodash/releases)
- [Commits](https://github.com/lodash/lodash/compare/4.17.21...4.18.1)

---
updated-dependencies:
- dependency-name: lodash
  dependency-version: 4.18.1
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com>
2026-05-06 16:47:05 +00:00
8ab68b574f Backoffice: Add localize.htmlString() helper to prevent XSS in HTML-rendered translations (#22731)
* docs(claude): document how unsafeHTML should be used together with escapeHTML()

* fix: adds escapeHTML where appropriate in order not to render html directly

* chore: removes small nitpick fallback

* docs(claude): fixes incorrect using of unsafeHTML

* feat(localization): add localize.htmlString() and convert call sites

Adds a new `htmlString()` method on UmbLocalizationController that escapes
interpolated args via escapeHTML and returns a Lit unsafeHTML directive.
This is the safe replacement for the manual `unsafeHTML(this.localize.string(...))`
pattern, which leaves user-controlled args un-escaped (XSS hazard).

Converts all direct `unsafeHTML(localize.string|term(...))` call sites
across modals, rollback views, packager, property editors, and entity
actions. Also fixes the latent XSS in `trash.action.ts` (sibling of the
previously-fixed `delete.action.ts`).

Updates docs/security.md with guidance on `string()` vs `htmlString()`
and the modal-content wrapping pattern.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore(eslint): add no-unsafe-localize rule to flag unsafeHTML(localize.string|term(...))

Catches the XSS pattern this PR's helper replaces, so future regressions
are caught at lint time instead of in review (or in a security advisory).
Suggests `localize.htmlString(...)` as the safe replacement.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(localization): stringify htmlString args before escaping

Addresses review feedback on PR #22731. escapeHTML() short-circuits on
non-strings (returns the value unchanged), so an arg like
{ toString: () => '<script>...</script>' } would bypass the escape and
render unescaped via unsafeHTML.

Stringifies args before escaping while preserving `undefined` so
string()'s placeholder semantics are unchanged. Adds a regression test
covering the toString() bypass.

Also adds the missing html/unsafeHTML imports to the security.md
example so the snippet is self-contained.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(installer-consent-element): sanitise content before rendering it

* fix(dashboard-telem-element): sanitise html before rendering

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: LLaverty <liamlaverty@gmail.com>
2026-05-06 16:11:47 +02:00
7c6b755ca5 Backoffice: Add localize.htmlString() helper to prevent XSS in HTML-rendered translations (#22731)
* docs(claude): document how unsafeHTML should be used together with escapeHTML()

* fix: adds escapeHTML where appropriate in order not to render html directly

* chore: removes small nitpick fallback

* docs(claude): fixes incorrect using of unsafeHTML

* feat(localization): add localize.htmlString() and convert call sites

Adds a new `htmlString()` method on UmbLocalizationController that escapes
interpolated args via escapeHTML and returns a Lit unsafeHTML directive.
This is the safe replacement for the manual `unsafeHTML(this.localize.string(...))`
pattern, which leaves user-controlled args un-escaped (XSS hazard).

Converts all direct `unsafeHTML(localize.string|term(...))` call sites
across modals, rollback views, packager, property editors, and entity
actions. Also fixes the latent XSS in `trash.action.ts` (sibling of the
previously-fixed `delete.action.ts`).

Updates docs/security.md with guidance on `string()` vs `htmlString()`
and the modal-content wrapping pattern.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore(eslint): add no-unsafe-localize rule to flag unsafeHTML(localize.string|term(...))

Catches the XSS pattern this PR's helper replaces, so future regressions
are caught at lint time instead of in review (or in a security advisory).
Suggests `localize.htmlString(...)` as the safe replacement.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(localization): stringify htmlString args before escaping

Addresses review feedback on PR #22731. escapeHTML() short-circuits on
non-strings (returns the value unchanged), so an arg like
{ toString: () => '<script>...</script>' } would bypass the escape and
render unescaped via unsafeHTML.

Stringifies args before escaping while preserving `undefined` so
string()'s placeholder semantics are unchanged. Adds a regression test
covering the toString() bypass.

Also adds the missing html/unsafeHTML imports to the security.md
example so the snippet is self-contained.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(installer-consent-element): sanitise content before rendering it

* fix(dashboard-telem-element): sanitise html before rendering

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: LLaverty <liamlaverty@gmail.com>
2026-05-06 16:11:14 +02:00
2ac2b3e4fa Fix main branch after merge issue (#22729)
* Revert "MD files for Design knowledge (#22725)"

This reverts commit 212f3183c1.

* Revert "Backoffice Mocks: Derive user language access from user groups (#22721)"

This reverts commit 9671fec9ad.

* Revert "File-system Services: Complete child scopes on read-miss and validation-failure paths (#22717)"

This reverts commit 489d9ebc2e.

* Revert "manual revert of merge gone wrong"

This reverts commit a443f8ba08.

* Revert "fix(installer-user): added min length message for installer user elem… (#21829)"

This reverts commit 6789d7e757.

* Reapply "Merge branch 'main' of https://github.com/umbraco/Umbraco-CMS into claude/keen-nightingale-5ef5bd"

This reverts commit daecbd02b8.

* fix(installer-user): added min length message for installer user elem… (#21829)

* fix(installer-user): added min length message for installer user element.

* Update src/Umbraco.Web.UI.Client/src/apps/installer/user/installer-user.element.ts

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Fix password minlength message binding syntax

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Emma L Garland <1649855+emmagarland@users.noreply.github.com>
Co-authored-by: Niels Lyngsø <nsl@umbraco.dk>

* File-system Services: Complete child scopes on read-miss and validation-failure paths (#22717)

* Ensure scopes in FolderServiceOperationBase are completed.

* Added integration tests to verify the fixes.

* Backoffice Mocks: Derive user language access from user groups (#22721)

fix(mocks): derive user language access from user groups

Previously hasAccessToAllLanguages was hardcoded to true and languages to
an empty array. Now both are derived from the user's user group memberships.

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>

* MD files for Design knowledge (#22725)

* Fix issues following merge.

* Fixed linting errors.

* Fix linter errors (2).

* Restore current-user.context.ts

* Restore block-list-entry.element.ts.

* Removed failing webhook repository test files.

---------

Co-authored-by: Yari Mariën <75362020+Yinzy00@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Emma L Garland <1649855+emmagarland@users.noreply.github.com>
Co-authored-by: Niels Lyngsø <nsl@umbraco.dk>
Co-authored-by: Mads Rasmussen <madsr@hey.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-06 15:01:05 +02:00
Andy Butland 626f0a9ee1 Bump version to 17.4.0-rc3. 2026-05-06 11:39:17 +02:00
Niels LyngsøandGitHub 212f3183c1 MD files for Design knowledge (#22725) 2026-05-06 09:26:28 +00:00
Niels Lyngsø 38c68ef384 Merge branch 'v17/hotfix/22472' 2026-05-06 10:39:09 +02:00
9671fec9ad Backoffice Mocks: Derive user language access from user groups (#22721)
fix(mocks): derive user language access from user groups

Previously hasAccessToAllLanguages was hardcoded to true and languages to
an empty array. Now both are derived from the user's user group memberships.

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-06 09:29:53 +02:00
Andy ButlandandGitHub 489d9ebc2e File-system Services: Complete child scopes on read-miss and validation-failure paths (#22717)
* Ensure scopes in FolderServiceOperationBase are completed.

* Added integration tests to verify the fixes.
2026-05-06 13:32:39 +09:00
Mads Rasmussen 9e34b76bf0 Merge branch 'main' of https://github.com/umbraco/Umbraco-CMS 2026-05-05 22:29:56 +02:00
Mads Rasmussen a443f8ba08 manual revert of merge gone wrong 2026-05-05 22:29:35 +02:00
6789d7e757 fix(installer-user): added min length message for installer user elem… (#21829)
* fix(installer-user): added min length message for installer user element.

* Update src/Umbraco.Web.UI.Client/src/apps/installer/user/installer-user.element.ts

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Fix password minlength message binding syntax

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Emma L Garland <1649855+emmagarland@users.noreply.github.com>
Co-authored-by: Niels Lyngsø <nsl@umbraco.dk>
2026-05-05 22:26:40 +02:00
Mads Rasmussen daecbd02b8 Revert "Merge branch 'main' of https://github.com/umbraco/Umbraco-CMS into claude/keen-nightingale-5ef5bd"
This reverts commit 0c57e304f8, reversing
changes made to 7c7073428d.
2026-05-05 22:12:39 +02:00
Mads Rasmussen 0c57e304f8 Merge branch 'main' of https://github.com/umbraco/Umbraco-CMS into claude/keen-nightingale-5ef5bd 2026-05-05 22:10:55 +02:00
ede972f711 Radio button list: Not saving value on keyboard navigation (closes #22698) (#22699)
Fix radio button list not saving value on keyboard navigation

Co-authored-by: Niels Lyngsø <niels.lyngso@gmail.com>
2026-05-05 19:50:59 +00:00
Mads RasmussenandGitHub 2c88f2ae3c Current User: Fix reload not fetching fresh data when entity events fire (#22719)
* Ensure current-user reloads fetch fresh data

* Update current-user.context.test.ts
2026-05-05 21:32:35 +02:00
d40c959be7 Dashboard: Browser title + Hints (#22517)
* View Contexts for Dashboards + Section Views to support Browser Title and Hints

* fix code

* use alias for observe ctrl alias

* remove test code

* Position badge in section icon slot

---------

Co-authored-by: engjlr <enl@umbraco.dk>
2026-05-05 21:26:37 +02:00
77ded81eff Languages: Sort the global content language selector (closes #22628) (#22711)
* Align sorting of content language selector with variant selector.

* Hoist sortLanguages helpers to module scope.

---------

Co-authored-by: Engiber Lozada <89547469+engijlr@users.noreply.github.com>
2026-05-05 18:33:07 +02:00
b3a9f86fe0 SignalR: Add configurable transport settings for load-balanced deployments without sticky sessions (#22700)
* WIP

* Cleanup and type generation

* Improve obsoletions

* Fix removed constructor

* Simplify logic because of SignalR's JS limitations

* Apply suggestions from code review

Co-authored-by: Andy Butland <abutland73@gmail.com>

* Add SignalRSettings to Schema

* Abstrack SignalRRoutes class

* Fix bool to observable<bool>

* Refactor base class: pull down common service property, make abstract with protected constructor.

---------

Co-authored-by: Andy Butland <abutland73@gmail.com>
2026-05-05 14:51:45 +00:00
Andy ButlandandGitHub c7d055a6e2 Caching: Invalidate published content type cache for element types (#22704)
* Ensure content type cache is correctly invalidated for element types.

* Clear key to Id map on clear all.

* Refactor and update tests for additional coverage and naming alignment.

* Updates from code review.
2026-05-05 13:20:37 +02:00
9e930739fb Tags: Close suggestion dropdown on blur and escape (closes #22636) (#22650)
* Close suggestion dropdown on blur and escape, fix suggestion selection

* Fix code complex

* Fix to tab and complexity

* Fix to tab and complexity

* Fix to tab and complexity

* Clear matches on add/escape and remove focus rule

---------

Co-authored-by: engjlr <enl@umbraco.dk>
Co-authored-by: Engiber Lozada <89547469+engijlr@users.noreply.github.com>
2026-05-05 09:39:41 +00:00
727581b88b State System: more tests, MD updates and a tiny bit more consistency (#22673)
* unit test for boolean state

* improve umb class state set value identical check

* consistent ability to make a observablePart

Co-authored-by: Copilot <copilot@github.com>

---------

Co-authored-by: Copilot <copilot@github.com>
2026-05-05 09:33:24 +00:00
b49929af97 Backoffice: Introduce Value Type and Value Summary extensions (#22481)
* Add table collection view and manifests

* Use table kind in collection example

* Update entity-name-table-column-layout.element.ts

* Recompute table rows when item hrefs change

* define and render columns from manifest

* wip language implementation

* map to unique field

* rename to label

* test implementation for users table

* experiment: value minimal display extension

* register as workspace context

* add boolean display

* clean up

* add example entity actions

* add example description

* Update table-collection-view.element.ts

* Omit base 'meta' and relax table meta type

* Hardcode description column when present

* localize column names

* Update table-collection-view.element.ts

* Type manifest on collection view elements

* Use UmbLitElement instead of LitElement

* fix types

* Update entity-name-table-column-layout.element.ts

* provide entity context for each table row

* fix breaking change and introduce a deprecation warning

* Add status column to example collection view + localize column labels

* implement the UmbTableColumnLayoutElement interface

* add tests for the table collection view

* Make host element optional; add table docs/types

* Update controller-host.mixin.ts

* Update src/Umbraco.Web.UI.Client/src/packages/core/entity-action/global-components/entity-actions-table-column-view/entity-actions-table-column-view.element.ts

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Add language collection context

* introduction of value type and rename to value summary

* Add core DateTime value summary; migrate user last-login

* remove unused

* Rename user group value type to References

* Remove the component's standalone resolution path

* Add start-node value summaries & sections for user group table

* Guard resolver and render when start node missing

* refactor value-summary resolver, coordinator, and API

* introduce default kind

* remove $ in variable name

* make extension element name more specific to not collide with interface name

* add element base

* move to section module

* return as observable from resolver

* use extension item repository

* prefix start node feature with user

* add value type and value summary for date-time-with-time-zone property editor

* render timezone

* Add fallback render if no extensions can be found

* Add color-picker value summary and types

* add summary for slider + align types

* make manifest prop name more explicit

* align element name with class name

* reorganize

* manually combine imports to decrease the number of dynamic imports

* export as valueResolver instead of api

* Inline default value-summary kind manifest

* Use single raw value in value-summary coordinator

* Render summaries on Document Collection cards

* format date the same way as the property editor

* first iteration of docs and skills

* updates to docs + skills

* render icon for language collection items

* remove test collection manifest

* delete local language table collection view implementation

* implement the get hrefs method in the user group collection context

* Update controller-host.mixin.ts

* Update entity-name-table-column-layout.element.ts

* Update entity-actions-table-column-view.element.ts

* Handle undefined row element in table rendering

Allow onRowRendered to accept an undefined element and clean up row contexts when a row is unmounted. Update the callback signature in table.element.ts and handle the undefined case in table-collection-view.element.ts by destroying the host and removing the stored context for the item to avoid memory leaks when rows are removed.

* Update controller-host.mixin.ts

* remove test registration

* Prefix type in value key generation

* Skip render when boolean value is undefined

* Add JSDoc and reorder imports in coordinator

* fix lint errors

* Update icons.ts

* valueResolver to class in tests

* Update index.ts

* Add value-summary and value-type Vite entries

* Cache table config and column cell elements

* Use localization for user state labels

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Niels Lyngsø <nsl@umbraco.dk>
Co-authored-by: Engiber Lozada <89547469+engijlr@users.noreply.github.com>
2026-05-05 09:07:39 +00:00
984838433c MD: improve knowledge on get vs consume context (#22676)
* improve Md regarding get vs consume context

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Mads Rasmussen <madsr@hey.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-05-05 10:14:33 +02:00
Andreas Lykke BorgandGitHub cf2b519ff9 Accessibility: Added missing labels to add property and create new collection (#22701)
Add missing label attributes to form controls
2026-05-05 07:02:52 +02:00
Andy Butland a773ba168b Merge branch 'release/17.4.0' 2026-05-04 23:00:48 +02:00
Andy ButlandandClaude Opus 4.7 cd406fba43 Remove npm/docs manual approval gates, keep MyGet-cascade fix.
The manual approval gates added for the duplicate-version rerun were
single-use scaffolding for that specific release. Remove them and
tighten Deploy_Npm and Upload_API_Docs to require Deploy_NuGet to
have actually succeeded (Succeeded or SucceededWithIssues) — so a
NuGet failure deliberately blocks the npm release and docs upload.

Keep the structural change to inspect dependencies.Deploy_NuGet.result
directly rather than rely on the transitive succeeded(). That fix is
permanent: it's what protects npm and docs from cascade-skipping
whenever MyGet has another upstream outage.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-04 21:26:25 +02:00
Andy ButlandandClaude Opus 4.7 caeb354064 Allow npm release and API docs upload to run when MyGet or NuGet fails.
Both stages used implicit succeeded(), which is transitive across the
full ancestor graph. A MyGet failure (or a NuGet failure on a re-run
where the version is already published) would therefore cascade-skip
both stages even though their own work is independent of those feeds.

Switch them to inspect dependencies.Deploy_NuGet.result directly so
they remain eligible when NuGet ran and either succeeded or failed,
while still being skipped when Deploy_NuGet itself was Skipped (e.g.
non-release runs). Upload_API_Docs additionally requires Build_Docs
to have produced artifacts.

Add a manual approval gate (ManualValidation@0 server job) to each
stage so a NuGet failure caused by something genuinely unrecoverable
(e.g. expired API key) doesn't auto-promote npm or docs publishes -
the operator must explicitly approve each downstream stage.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-04 20:19:53 +02:00
Engiber LozadaandGitHub 8f6bb64ced Content Workspace: Add variant sync when switching app culture (closes #16853) (#22566)
* Sync workspace URL on language change

* Use template literals for workspace paths

* Move and improve culture URL sync logic
2026-05-04 17:01:24 +00:00
dec99737b7 Build pipeline: Add manual approval gate to NuGet release (#22695)
* Add manual deploy to NuGet for when MyGet publish fails.

* Simplified instructions for manual approval.

* Gate NuGet release on MyGet's direct result, not transitive succeeded/failed.

succeeded() and failed() are transitive across the full ancestor graph,
so a failure in Unit/Integration/E2E (which skips Deploy_MyGet) still made
or(succeeded(), failed()) evaluate to true and opened the approval gate
on a broken build. Inspect dependencies.Deploy_MyGet.result instead so
Deploy_NuGet only becomes eligible when MyGet itself actually ran.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-04 18:43:09 +02:00
Niels LyngsøandGitHub 4e7bf3c483 Validation: Data lookup mismatch for JSON Path Queries (#22609)
* unit tests to prove issue

* ensure full match for json path filter query

* check for null value

* remove comment

* remove comment

* remove comment
2026-05-04 15:02:39 +02:00
7136e12e54 Property Editor UI Picker: Implement fuzzy search (#22468)
* extend icons with information from theseaurus

* implement new icon search logic

* clean-up data

* sorting with a backup of the name

* refactor into a controller

* improve multi word group search

* embed lucide data

* rename tech into technology

* remove paper from dollar

* implement fuzzy search for property editor UIs

* minor style update

* improve property editor UI search

* improve search

* improve search data for Property Editor UIs

* remove alias search from property editor ui search

* add usage keywords

* Property editor Suggestions based on Property Label

* related should not show up in search

* rename to suggestionQuery

* update threshold

* separate name words

* also consider full icon name match

* better comment

* other approach for full name matches

* full icon name search if query contains a -

* fix test

* cache all tokens as well

* catch rejection

* resolve feedback

* handle rejected promise

* cancel debounce on disconnect

Co-authored-by: Copilot <copilot@github.com>

* declare voids

* corrections

Co-authored-by: Copilot <copilot@github.com>

* back out if no tokens

---------

Co-authored-by: Copilot <copilot@github.com>
2026-05-04 12:28:25 +00:00
Nicklas KramerandGitHub f1f215a3a8 User management: Improved error message when deleting active user (closes #22669) (#22687)
* Adding a more detailed error message when deleting a logged in user

* Fixing overlooked integration test

* Fixing enum binary mistake. Appending enum to the end rather than in the middle.

* Introducing better naming for the enum
2026-05-04 12:12:07 +00:00
Niels LyngsøandGitHub 7e675e243b Claude MD: Code Comments (#22690)
* initial commit

* improve docs
2026-05-04 11:45:55 +00:00
5bd0b720a0 Document picker: show ancestor breadcrumb path in document picker search results (closes #22645) (#22649)
* Show ancestor breadcrumb path in RTE picker search results

* hide tree when searching

* use clear localization instead of delete

---------

Co-authored-by: Niels Lyngsø <nsl@umbraco.dk>
Co-authored-by: Mads Rasmussen <madsr@hey.com>
2026-05-04 11:36:40 +00:00
Nhu DinhandGitHub 1215d83b0f E2E: QA Added acceptance tests for backoffice login, logout and reset password (#22635)
* Added api helper for reset auth state

* Added more constant variables for login and forgot password message

* Added ui helper for login page

* Added api helper for smtp

* Added tests for backoffice login

* Added tests for backoffice logout

* Added tests for forgot password

* Added api helper for user

* Make tests run in the pipeline

* Updated appsetting to enable reset password

* Added more waits

* Added waits

* Updated locator

* Fix flaky tests

* Updated confirmation message

* Fixed comments

* Removed unused code

* Reverted npm command
2026-05-04 10:30:27 +00:00
349e9d1130 Blueprints: Fix intermittent blank workspace when creating documents from blueprints (closes #21996) (#22422)
* Resolve blank workspace when creating documents from blueprints.

* Addressed code review feedback.

* Revert defensive fixes that don't appear to contribute to fixing the bug.

* remove comment

---------

Co-authored-by: Niels Lyngsø <niels.lyngso@gmail.com>
2026-05-04 10:28:03 +00:00
Niels LyngsøandGitHub bbebb07e8c Blueprints: Fix creating documents from blueprints (closes #21996) (#22688)
cherry picked fix from #22422
2026-05-04 10:03:31 +00:00
0eef8e6b31 Code Quality: Add ModelState validation to BackOfficeLoginController (#22681)
* Add ModelState.IsValid validation in controller action

* Update method documentation and return simple BadRequest response (aligns with other usages, e.g. BackOfficeController.Verify2FACode).

---------

Co-authored-by: Lan Nguyen Thuy <lnt@umbraco.dk>
Co-authored-by: Andy Butland <abutland73@gmail.com>
2026-05-04 09:44:50 +00:00
f2dc9e7031 Block permissions: Correction of read-only inheritance and language access (#22522)
* remove inheritance of readonly state

* keep rendering edit in read-only mode

* INVARIANT variant id as static

* parse readonly state, without variant ids as origin is the property read-only state

* stop inheriting read only

* no need for async

* setup read only state based on user permissions

* simplify document-block-property-level-permissions

* make isPermittedForObservableVariant return undefined in bad case

* revert

* improve life cycle for extension initializer

* fix and clean-up

* clean up

* unit test for the actual problem

* clean up

* clean up

* revert logic

* transform access context into local controller

* re-introduce submit create button

* simplify match

* update js docs

* strict compare on config object level, to cover multiple conditions of the same alias.

* Revert "transform access context into local controller"

This reverts commit 1a83d9586b.

* rename file in manifest

* RTE: set manager readOnly

* set fallback on readOnly

* inherit readOnly state when block workspace is invariant

* read-only tag for Block Workspace

* make guard fallback reactive

* observe readOnly languages

* no if sentence

* observe fallback for property + name guards

* prevent cancelled context get to cause problems

* revert removal of  || this._isReadOnly check for component rendering

* add comment for clarification

* remove style import

* mark as readonly and make js-const

* remove `as const`

* unit test for reactive fallback feature

* more guard unit tests

* more variantId tests

* move block language access controller to block package

* Update base-extension-initializer.controller.ts

* fix test

* improve switch condition

* offset condition

* Block Workspace: Add data-mark for acceptance test locator

* apply entity-type to the workspace data-mark

* layout-headline

* Updated locator to use new data-mark

* Updated tests to make them less fragile

* null ctrl alias for constructor initiated observations

* import directly

* do not react to not existing user-data or missing context

* add comment

* refactor package registration logic

* package name for code editor

* leave unregistere out

* await load all bundles

Co-authored-by: Copilot <copilot@github.com>

* move initializer to app element

* Batch register extensions with validation

* remove await on load for extension initializers

* Debounce extension updates and set loaded flag

* remove unused imports

* refactor backoffice -> app

* clean up imports

* rename comment

Co-authored-by: Copilot <copilot@github.com>

* base extension initializer is loaded update

* app loader

Co-authored-by: Copilot <copilot@github.com>

* embed umbraco-packages

* remove lazy loads from dataSourceDataMapper

* revert

* enable routes to be undefined

Co-authored-by: Copilot <copilot@github.com>

* comment

Co-authored-by: Copilot <copilot@github.com>

* make sure load only calls once

Co-authored-by: Copilot <copilot@github.com>

* comments and todos

* destroy consumer if existing

* block language access tests

* load user at the end of loading all package modules

* assign symbol for is-trashed observer

* revert language readonly rules

Co-authored-by: Copilot <copilot@github.com>

* is-trashed context + observation

Co-authored-by: Copilot <copilot@github.com>

* read-only as view prop for block list

Co-authored-by: Copilot <copilot@github.com>

* readonly as view prop

* readonly prop for grid,rte,single

Co-authored-by: Copilot <copilot@github.com>

---------

Co-authored-by: Mads Rasmussen <madsr@hey.com>
Co-authored-by: Andreas Zerbst <andr317c@live.dk>
Co-authored-by: Copilot <copilot@github.com>
2026-05-04 09:40:34 +00:00
fe413cd0ae Document Type Workspace: Hide non-applicable settings when Document Type is configured as Element Type (#22388)
* Avoid render structure view when element type is active

* Avoid render history clean up when is an element type

* Replace hidden sections with inline "not applicable" message for Element Types

---------

Co-authored-by: Mads Rasmussen <madsr@hey.com>
Co-authored-by: Niels Lyngsø <niels.lyngso@gmail.com>
2026-05-04 09:26:17 +00:00
6ce2ab9fe9 HttpClients: Deprecate unused HttpClient registered with certificate validation bypass (#22684)
Mark HttpClient IgnoreCertificateErrors as obsolete due to security risk and add TODO to remove in a future release

Co-authored-by: Lan Nguyen Thuy <lnt@umbraco.dk>
2026-05-04 10:32:11 +02:00
Andy Butland 5e1aabcce6 Bump version to 17.4.0-rc2. 2026-05-04 10:15:16 +02:00
8d961b1873 Blocks: Adds blockAction extension type (#22459)
* feat(block): add blockAction extension type for extensible block entry actions

Introduce a new `blockAction` extension type that allows both internal and
3rd-party extensions to register actions on block items. This replaces the
hardcoded Delete button on Block List entries with an extension-registered
action, while keeping Edit Content, Edit Settings, and Copy to Clipboard
as slotted content for incremental migration.

The new `<umb-block-action-list>` element owns the `<uui-action-bar>` and
renders a `<slot>` for hardcoded actions followed by extension-registered
`blockAction` extensions, enabling one-by-one migration of actions.

* feat(block): apply blockAction extension to grid, rte, and single block editors

Extend the blockAction pattern to all remaining block entry elements.
Each editor now uses <umb-block-action-list> with slotted hardcoded
actions and the Delete action registered via the extension registry.

* fix(block): render blockAction extensions directly in uui-action-bar

Replace umb-extension-with-api-slot with UmbExtensionsElementAndApiInitializer
to render blockAction elements as direct children of uui-action-bar. This fixes
the border-radius issue where the wrapper element broke :first-child/:last-child
structural selectors used by uui-action-bar for button styling.

* refactor(block): replace showOnReadOnly meta with BlockEntryIsReadOnly condition

Add a new Umb.Condition.BlockEntryIsReadOnly condition that checks the
read-only state from UMB_BLOCK_ENTRY_CONTEXT. This replaces the inline
read-only guard and showOnReadOnly meta flag on the default kind element.

Delete action uses the condition with match: false (hidden when read-only).
Copy to Clipboard has no condition (always visible). 3rd-party actions
opt in to read-only gating by adding the condition to their manifest.

* feat(block): migrate clipboard copy to blockAction extension

Move the Copy to Clipboard action from hardcoded buttons to a registered
blockAction extension across all four block editors. The copy logic is
moved from each entry element into its respective entry context, with a
base copyToClipboard() method on UmbBlockEntryContext.

* docs(block): add plan for migrating Edit Content and Edit Settings to blockAction

* feat(block): migrate Edit Settings to blockAction extension

Replace the hardcoded Edit Settings button with a blockAction extension
using the default kind. The API class provides getHref() for workspace
navigation and getValidationDataPath() for the invalid badge.

Adds getValidationDataPath() to the UmbBlockAction interface and default
kind element, enabling any blockAction to display a validation badge.

Introduces Umb.Condition.BlockEntryHasSettings condition to control
visibility based on whether the block has a settings element type.

* feat(block): migrate Edit Content to blockAction extensions

Split the hardcoded Edit Content button into two blockAction extensions
controlled by manifest conditions:

- Umb.BlockAction.EditContent — navigates to workspace content view,
  shows validation badge via getValidationDataPath()
- Umb.BlockAction.ExposeContent — calls context.expose() when block
  is not yet exposed and content edit is hidden

Adds match support to BlockEntryShowContentEdit condition and creates
a new BlockEntryIsExposed condition at the entry level.

Removes the <slot> from umb-block-action-list — all block entry actions
are now fully driven by the extension registry.

* Removes plan/spec files

* chore(block): address review findings for blockAction feature

- Add TODO comment for stale getHref/getValidationDataPath (I-1)
- Remove orphaned @state() properties from all four entry elements (I-2)
- Add UMB_BLOCK_ENTRY_SHOW_CONTENT_EDIT_CONDITION_ALIAS constant and
  replace string literals in edit-content/expose-content manifests (I-3)
- Change Expose Content weight from 400 to 399 (S-1)
- Add JSDoc to exported types and classes (S-2)
- Fix condition import alias — rename workspace-level to
  UmbBlockWorkspaceIsExposedCondition (S-3)

* fix(block): revert CSS custom property rename to preserve backwards compatibility

Restore the original per-editor CSS custom property names:
--umb-block-list-entry-actions-opacity, --umb-block-grid-entry-actions-opacity,
--umb-block-single-entry-actions-opacity. The action bar opacity styles are
now back in each entry element (using #actions selector), so the unified
property name is no longer needed.

* fix(block): address PR review feedback from Copilot and Claude bots

- Fix Expose button label regression — replace dynamic
  '#blockEditor_createThisFor' (function key) with static '#actions_create'
  so the button no longer renders "Create undefined"
- Guard empty-string href in EditContent and EditSettings actions —
  'workspaceEdit{Content,Settings}Path' emits '' before ready; return
  undefined instead of '' so the button doesn't get href="" (which would
  navigate to the base URL on click)
- Clear _href in default kind api setter — prevents stale href when the
  api is replaced or set to undefined
- Fix barrel imports in 3 block entry conditions — import
  UMB_BLOCK_ENTRY_CONTEXT directly from context-token.js rather than via
  the ../index.js barrel, reducing circular dependency risk
- Make block-action-list reactive to contentTypeAlias changes — the
  extensions initializer is now re-created when unique or
  contentTypeAlias changes, so forContentTypeAlias filters apply
  correctly when contentTypeAlias resolves asynchronously
- Throw in base copyToClipboard() — the default no-op on
  UmbBlockEntryContext now throws rather than logging a warning, so any
  future subclass that fails to override fails visibly

Tests for the new conditions were attempted but deferred to follow-up;
context observable mocking semantics need more investigation.

* fix(block): restore uui-action-bar styling on block-action buttons

Remove the `compact` attribute from the inner `<uui-button>` and bridge
the CSS custom properties set by `uui-action-bar::slotted(*:first-child)`
etc. through `<umb-block-action>`'s shadow DOM via intermediate
`--umb-button-*` variables. Without this bridge, `uui-button`'s own
`:host` declarations shadow the inherited values and the first/last
button border-radius + padding don't apply.

* fix(block): address second-pass PR review feedback

- Throw when RTE editor manifest is missing so clipboard entries are
  never written with an empty propertyEditorUiAlias (would silently
  fail to match on paste)
- Replace bare `return` with `return nothing` in default kind element
  render() for type-level clarity
- Add class-level JSDoc to exported block action classes
  (UmbEditContentBlockAction, UmbEditSettingsBlockAction,
  UmbDeleteBlockAction, UmbCopyToClipboardBlockAction,
  UmbExposeContentBlockAction) and UmbBlockActionDefaultElement

* refactor(block): reduce copyToClipboard complexity per CodeScene feedback

Extract `#buildPropertyValue()` helper in List, RTE, and Single entry
contexts to move the four content/layout/settings/expose ternaries out
of copyToClipboard, lowering its cyclomatic complexity.

Split the compound `||` context guards into sequential early-return
checks so each missing context throws with a specific error message,
and the "Complex Conditional" smell is removed.

* refactor(block): further reduce RTE copyToClipboard complexity

Consolidate three sequential `await getContext(...)` calls into a single
`Promise.all`, dropping the cyclomatic complexity below CodeScene's
threshold of 9.

* refactor(block): extract RTE clipboard write into helper method

Split the post-guard write phase into `#writeClipboardEntry` to bring
both methods well under CodeScene's cyclomatic complexity threshold.

* clean up action

Co-authored-by: Copilot <copilot@github.com>

* show edit content / settings despite read-only state

---------

Co-authored-by: Niels Lyngsø <niels.lyngso@gmail.com>
Co-authored-by: Copilot <copilot@github.com>
2026-05-04 08:07:16 +00:00
Andy ButlandandGitHub 013d55ef30 Routing: Ensure IPublishedContent.UrlSegment respects umbracoUrlName (closes #22655) (#22663)
* Align obsolete UrlSegment with result of replacement service call.

* Resolved warnings in tests.

* Addressed code review feedback.

* Fix failing integration tests.

* Clarified handling of documents.

* Fix failing unit tests.

* Fixed further faliing integration test.
2026-05-04 10:29:00 +09:00
e414d05b9d Localization: Use invariant culture when parsing node paths (closes #22610) (#22625)
* Use InvariantCulture when parsing node paths.

* Add suggested validation of setup to integration test.

* Add more explicit tests for negative sign handling

---------

Co-authored-by: kjac <kja@umbraco.dk>
2026-05-03 07:40:12 +00:00
Andy ButlandandGitHub 52c133690d Media: Record the trashing user against the History audit entry (closes #22661) (#22668)
Ensure the trashing user for media is associated with the audit log entry.
2026-05-03 09:15:43 +02:00
Niels Lyngsø f61adcc1c0 improve acceptance test 2026-05-01 23:04:23 +02:00
Niels Lyngsø 64525c201f specify app loader + acceptance test queries 2026-05-01 22:13:49 +02:00
Andreas Lykke BorgandGitHub a49008b9f8 Accessibility: Added missing labels to number fields in the settings tab (#22667)
Added missing labels to fix console warning
2026-05-01 17:16:32 +02:00
Laura NetoandGitHub ef5d95a4c2 Merge branch 'release/17.4.0' into v17/hotfix/22472 2026-05-01 15:49:57 +02:00
Kenn JacobsenandGitHub f08bd93793 Cherry-picked the missing constant for "unroutable" (#22672) 2026-05-01 15:49:16 +02:00
Niels Lyngsø 06fb49fe7a make unit test only test output 2026-05-01 11:49:33 +02:00
Niels Lyngsø ab8e59a43f remove unused import 2026-05-01 11:44:30 +02:00
Niels Lyngsø 6037f7664c remove trash context for blocks 2026-05-01 11:36:16 +02:00
Niels Lyngsø 41ab7cbbab remove type cast 2026-05-01 11:22:06 +02:00
Niels LyngsøandCopilot 93a2d65702 JSDocs for INVARIANT umbVariantId
Co-authored-by: Copilot <copilot@github.com>
2026-05-01 11:21:37 +02:00
Niels Lyngsø 62436a4c7f resolve load promise feedback 2026-05-01 11:20:48 +02:00
Niels LyngsøandCopilot 424a5060f8 fix typescript typings
Co-authored-by: Copilot <copilot@github.com>
2026-05-01 11:19:23 +02:00
Niels Lyngsø 715831ae2a remove unused import 2026-05-01 11:09:03 +02:00
Niels Lyngsø afa0fab3fb back out if not available 2026-05-01 11:09:02 +02:00
Andreas Zerbst 1845a610a3 Makes helpers more robust by adding a hover step 2026-05-01 11:05:14 +02:00
Niels LyngsøandGitHub 59432bbbed Merge branch 'release/17.4.0' into v17/hotfix/22472 2026-05-01 10:06:03 +02:00
Niels LyngsøandCopilot a00d38eb04 readonly prop for grid,rte,single
Co-authored-by: Copilot <copilot@github.com>
2026-05-01 09:55:15 +02:00
Niels Lyngsø 2a4cdcf884 readonly as view prop 2026-05-01 09:53:44 +02:00
Niels LyngsøandCopilot efc862d301 read-only as view prop for block list
Co-authored-by: Copilot <copilot@github.com>
2026-05-01 09:53:23 +02:00
Andy ButlandandZeegaan 79e7b95253 Redirect Tracker: Prevent creation of redirects from unrouteable URLs (closes #22652, #22256) (#22657)
* Prevent creation of redirects when the old route is unroutable.

* Addressed code review feedback.

* Extend fix to handle case where a second, child page is "redirected" after preview was left open.

(cherry picked from commit 728789aaf6)
2026-05-01 16:31:30 +09:00
Zeegaan d76daf5b4e bump version 2026-05-01 16:30:27 +09:00
Niels LyngsøandCopilot 1568589576 is-trashed context + observation
Co-authored-by: Copilot <copilot@github.com>
2026-05-01 09:23:38 +02:00
Niels LyngsøandCopilot e61e0b6f51 revert language readonly rules
Co-authored-by: Copilot <copilot@github.com>
2026-05-01 09:23:27 +02:00
1edf7e9853 Ensure published querying parity between V13 and V17 (#22622)
* Ensure published querying parity between V13 and V17

* Add unit tests for published ancestor path querying

* Fix Claude review comments

* Make Unfiltered() public on the interface

* Explicitly evaluate "unfiltered" items

* A little clean-up

* Add integration tests

* Addressed code review feedback.

---------

Co-authored-by: Andy Butland <abutland73@gmail.com>
2026-05-01 09:06:04 +02:00
Niels Lyngsø 68b19a506c assign symbol for is-trashed observer 2026-05-01 08:37:09 +02:00
Kenn JacobsenandGitHub ae2318d8e9 Cache: Do not assume "published" when unpublishing a single culture (#22662) 2026-05-01 05:55:36 +02:00
Andy ButlandandGitHub 728789aaf6 Redirect Tracker: Prevent creation of redirects from unrouteable URLs (closes #22652, #22256) (#22657)
* Prevent creation of redirects when the old route is unroutable.

* Addressed code review feedback.

* Extend fix to handle case where a second, child page is "redirected" after preview was left open.
2026-05-01 09:04:22 +09:00
Andy Butland efe1f0fe59 Merge remote-tracking branch 'origin/release/17.3.5' 2026-04-30 15:16:43 +02:00
Andy Butland f4a9310ecc Merge branch 'release/17.3.5' 2026-04-30 15:15:54 +02:00
Niels Lyngsø 151d96f127 load user at the end of loading all package modules 2026-04-30 12:51:30 +02:00
Niels LyngsøandGitHub 1486121ffa V17/hotfix/revert parts of 21982 (#22656)
* do not inherit property write permissions

* revert hidding edit actions
2026-04-30 12:49:10 +02:00
Niels Lyngsø 5cd048fe67 block language access tests 2026-04-30 12:31:06 +02:00
Niels Lyngsø db5bd9ec50 destroy consumer if existing 2026-04-30 10:58:24 +02:00
Niels Lyngsø 0908586e89 update package-lock with version number 2026-04-30 10:21:29 +02:00
Niels Lyngsø c1f7a37d2a comments and todos 2026-04-30 10:17:09 +02:00
Andy Butland e6f53b9d30 Bump version to 17.3.5. 2026-04-30 10:14:57 +02:00
Niels LyngsøandCopilot a1620c9a31 make sure load only calls once
Co-authored-by: Copilot <copilot@github.com>
2026-04-30 10:00:45 +02:00
Niels LyngsøandCopilot b08e23d5ef comment
Co-authored-by: Copilot <copilot@github.com>
2026-04-30 09:53:25 +02:00
Niels LyngsøandCopilot e27c16e1a9 enable routes to be undefined
Co-authored-by: Copilot <copilot@github.com>
2026-04-30 09:45:34 +02:00
Niels Lyngsø 22d12449ac revert 2026-04-29 15:46:15 +02:00
Niels Lyngsø 2174b5f690 Merge remote-tracking branch 'origin/release/17.4.0' into v17/hotfix/22472 2026-04-29 15:34:03 +02:00
Niels Lyngsø 172ea1af59 remove lazy loads from dataSourceDataMapper 2026-04-29 15:32:27 +02:00
Niels Lyngsø d56c57cf2f embed umbraco-packages 2026-04-29 15:31:20 +02:00
Niels LyngsøandCopilot e65bacbdc8 app loader
Co-authored-by: Copilot <copilot@github.com>
2026-04-29 15:23:33 +02:00
Niels Lyngsø ce0f5e77e8 base extension initializer is loaded update 2026-04-29 15:23:27 +02:00
Niels LyngsøandCopilot 69258aadea rename comment
Co-authored-by: Copilot <copilot@github.com>
2026-04-29 14:35:28 +02:00
Niels Lyngsø 4d6b4b187b clean up imports 2026-04-29 14:32:45 +02:00
Niels Lyngsø 402e5dfa90 refactor backoffice -> app 2026-04-29 14:22:47 +02:00
Niels Lyngsø fc93fed936 remove unused imports 2026-04-29 14:14:54 +02:00
Mads Rasmussen 6306f3d4fd Merge branch 'v17/hotfix/22472' of https://github.com/umbraco/Umbraco-CMS into v17/hotfix/22472 2026-04-29 14:11:28 +02:00
Mads Rasmussen 586052bab1 Debounce extension updates and set loaded flag 2026-04-29 14:11:18 +02:00
Niels Lyngsø 87d8cab843 remove await on load for extension initializers 2026-04-29 14:11:08 +02:00
Mads Rasmussen 48973739aa Batch register extensions with validation 2026-04-29 13:58:34 +02:00
Mads Rasmussen 22c7e498d3 move initializer to app element 2026-04-29 13:54:46 +02:00
Andy Butland cc373296df Remove inadvertently committed research files from source control 2026-04-29 13:52:48 +02:00
Lee KelleherandGitHub e37a2919fc Content Rollback: Add notification message meta property (#22631)
* Extends `UmbContentRollbackModalValue` with `UmbEntityModel`

so that the Rollback modal can return the entity-type,
to display the correct notification message.

* Housekeeping

* Added localized fallback key

* Fixed typecasting issue for deprecated Document rollback

* Reverted logic, introduced `rollbackNotificationMessage` meta prop
2026-04-29 12:36:21 +01:00
Nhu DinhandGitHub b23b25163a E2E: QA Added acceptance tests for audit trail in content (#22479)
* Added constant variables for audit trail

* Added ui helper for audit trail

* Added tests for audit trails in content

* Added test for audit trail when trash content

* Added tests for audit trail when sort. move and rollback content

* Added tests for audit trail when bulk actions

* Updated tests for creating content

* Fixed comment
2026-04-29 17:37:21 +07:00
Isioma Nnodumandmole 0c021bedec bug(#22607) Add Directory.Packages.props and update restore command (#22608)
* bug(#22607) Add Directory.Packages.props and update restore command

Updated Dockerfile to include Directory.Packages.props and modified restore command to resolve docker build errors during dotnet restore step. Resolves issue #22607

* fix(template): conditionally copy Directory.Packages.props in Dockerfile

Only copy Directory.Packages.props when CPM is enabled, as per-project
package management users won't have this file in their build context.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: mole <nikolajlauridsen@protonmail.ch>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
(cherry picked from commit df3cd50e7f)
2026-04-29 11:08:08 +02:00
Mole 0af0c47f69 Docker Compose template: Improve secrets handling and add script to trust development certificates (#22613)
* Generate random guid for cert pass

* Changes from review

* Move cert generation and add script to trust cert on host machine

* Generate simple hmac key

(cherry picked from commit fcf5af3d16)
2026-04-29 11:08:04 +02:00
df3cd50e7f bug(#22607) Add Directory.Packages.props and update restore command (#22608)
* bug(#22607) Add Directory.Packages.props and update restore command 

Updated Dockerfile to include Directory.Packages.props and modified restore command to resolve docker build errors during dotnet restore step. Resolves issue #22607

* fix(template): conditionally copy Directory.Packages.props in Dockerfile

Only copy Directory.Packages.props when CPM is enabled, as per-project
package management users won't have this file in their build context.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: mole <nikolajlauridsen@protonmail.ch>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-29 11:06:38 +02:00
Niels LyngsøandCopilot 044950e0a4 await load all bundles
Co-authored-by: Copilot <copilot@github.com>
2026-04-29 10:32:32 +02:00
Niels Lyngsø 2572f6f0b5 leave unregistere out 2026-04-29 09:44:09 +02:00
Niels Lyngsø 89f5e49293 package name for code editor 2026-04-29 09:41:57 +02:00
Niels Lyngsø 323a731ed1 refactor package registration logic 2026-04-29 08:59:26 +02:00
Niels Lyngsø 24177dc62d add comment 2026-04-28 16:22:48 +02:00
Niels Lyngsø 7b351b199c do not react to not existing user-data or missing context 2026-04-28 16:22:38 +02:00
2f52b7b2b8 Redirect Url Management: Implement workspace (#22624)
* add redirect tracking workspace

* change weight to match v13 order

* add missing alignment and text colour

* Align closer with referency by element

* Ad repository pattern from review

* remove obsolete

* Update src/Umbraco.Web.UI.Client/src/packages/documents/documents/redirect-management/info-app/document-redirect-management-workspace-info-app.element.ts

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Adds JSDocs

* Removed unused `created` and `documentUnique` from `UmbDocumentRedirectUrlModel`

* Align `setStatus` and `delete` return shape with other data source methods

* Align workspace context observer with sibling info-app pattern

* Polish dashboard and info-app: localize hardcoded strings, tidy templates and imports

* Apply review simplifications

- Drop duplicate `unique` guards from data source (kept at repository boundary)
- Drop unnecessary `?? []` fallbacks (`items` is non-nullable in the API type)
- Localize hardcoded zero-results strings in dashboard
- Simplify redundant length check in info-app `#getTargetUrl`
- Drop unused `userIsAdmin` from `UmbDocumentRedirectStatusModel`

---------

Co-authored-by: leekelleher <leekelleher@gmail.com>
2026-04-28 14:22:02 +00:00
Niels Lyngsø f30178ebc5 import directly 2026-04-28 16:21:41 +02:00
Niels Lyngsø cd0a8b2478 null ctrl alias for constructor initiated observations 2026-04-28 14:56:28 +02:00
Andreas Zerbst 9bdc0709cc Updated tests to make them less fragile 2026-04-28 13:05:58 +02:00
Andreas Zerbst 632b0ae099 Updated locator to use new data-mark 2026-04-28 13:05:32 +02:00
Mads RasmussenandGitHub 3991c95c45 Current User: Reload when the current user or their groups change (#22623)
* Add action event listeners to current-user context

* Add current-user.context tests

* Update current-user.context.test.ts

* Debounce current user reloads caused by events
2026-04-28 11:52:12 +01:00
d467d57198 Rich Text Editor: Mark as supports read only (#22600)
* Mark RTE as supports read only

* RTE: Address read-only review feedback

- Remove `pointer-events: none` from `:host([readonly])` so users can select and copy text in read-only mode
- Make the editor's editable state reactive to the `readonly` property via `setEditable`
- Skip rendering the statusbar in read-only mode (mirrors the toolbar) to avoid the missing border-radius regression
- Remove the now-unused `readonly` property from `umb-tiptap-toolbar` and `umb-tiptap-statusbar`

---------

Co-authored-by: leekelleher <leekelleher@gmail.com>
2026-04-28 10:49:34 +00:00
Niels Lyngsø 4df3fc7867 layout-headline 2026-04-28 12:44:02 +02:00
6bd3edeea3 Current User: Adds Current User workspace modal (#22268)
* init current user workspace

* adding current user workspace and their apis

* add new controllers

* add default implementation

* Update src/Umbraco.Core/Services/UserService.cs

Co-authored-by: Andy Butland <abutland73@gmail.com>

* Update src/Umbraco.Cms.Api.Management/ViewModels/User/UpdateCurrentUserRequestModel.cs

Co-authored-by: Andy Butland <abutland73@gmail.com>

* Update tests/Umbraco.Tests.Integration/Umbraco.Core/Services/UserServiceCrudTests.Update.cs

Co-authored-by: Andy Butland <abutland73@gmail.com>

* Update src/Umbraco.Core/Models/CurrentUserUpdateModel.cs

Co-authored-by: Andy Butland <abutland73@gmail.com>

* update openApi.json, remove userKey from model, remove redundant authentication check from controllers

* update localize

* allow blob: URLs in img-src CSP for avatar

* save image change later

* Remove references to "current" user from service layer.
Align validation for user profile update with update user service method.
Controller tidy-up of dependencies.

* Add missing controller from last commit.

* resolve conflicts 2

* Renamed/relocated "current-user-workspace" to "profile/edit"

Refactored the "Edit" (profile) button logic,
to handle the check whether the user has access to the Users section.

* Removed the "Section User No Permission" condition

as no longer used.

* UI tweaks + streamlining

* Profile edit: surface save errors and avoid blob URL leak

- Show danger notification when avatar upload/delete or profile update fails
- Refresh current user after avatar upload so the store holds server URLs, not a leaking local blob
- Element save() methods now return boolean; modal keeps itself open when a save fails and no longer double-submits

* Refactored to use `asPromise()`

* Current User: Adapt edit-profile modal into a workspace extension

Replaces Umb.Modal.CurrentUserEditProfile with a workspace registered
against entityType 'current-user'. The UmbSubmittableWorkspaceContextBase
subclass owns the editable user model and pending avatar state; submit()
coordinates uploadAvatar / deleteAvatar / updateProfile and throws on
failure so the workspace stays open, relying on the repository's existing
danger notifications.

The current-user "Edit" action now opens UMB_WORKSPACE_MODAL (sidebar,
small) instead of the bespoke modal. Avatar and settings children become
presentational views wired to the workspace context.

* Current User workspace: Address review findings

- Await initial load promise in submit() to prevent a race where the save
  action fires before the first requestCurrentUser() resolves.
- Guard the avatar element's async observer setup against post-disconnect
  attachment.
- Document the split between #data (editable persisted state) and
  #pendingAvatar (transient UI state) in the workspace context.
- Remove stray JSDoc whitespace in current-user.server.data-source.ts.

---------

Co-authored-by: Lan Nguyen Thuy <lnt@umbraco.dk>
Co-authored-by: Andy Butland <abutland73@gmail.com>
Co-authored-by: leekelleher <leekelleher@gmail.com>
2026-04-28 11:18:13 +01:00
b49a0905d6 Repositories: Quote table and column names in raw SQL in MemberFilterRepository (closes #22615) (#22616)
* fix raw sql with ISqlSyntaxProvider name escaping

* reduce hard coded strings

* Fix Raw Sql in MemberFilterRepository

* fix formating

* remove migrations update changes

* Update src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MemberFilterRepository.cs

manually validated

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Minor code tidy.

* Add further integration tests.

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Andy Butland <abutland73@gmail.com>
2026-04-28 12:07:12 +02:00
Dirk SeefeldandAndy Butland 435c4abb42 Migrations: Fix raw SQL with ISqlSyntaxProvider table and column quoting (closes #22603) (#22604)
* fix raw sql with ISqlSyntaxProvider name escaping

* reduce hard coded strings

* Fix Raw Sql in MemberFilterRepository

* fix formating

* restore MemberFilterRepository

* Correct usage of field name constant.

---------

Co-authored-by: Andy Butland <abutland73@gmail.com>
2026-04-28 12:07:02 +02:00
57b063f3f4 Repositories: Quote table and column names in raw SQL in MemberFilterRepository (closes #22615) (#22616)
* fix raw sql with ISqlSyntaxProvider name escaping

* reduce hard coded strings

* Fix Raw Sql in MemberFilterRepository

* fix formating

* remove migrations update changes

* Update src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MemberFilterRepository.cs

manually validated

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Minor code tidy.

* Add further integration tests.

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Andy Butland <abutland73@gmail.com>
2026-04-28 09:33:59 +00:00
122e1a94d9 Migrations: Fix raw SQL with ISqlSyntaxProvider table and column quoting (closes #22603) (#22604)
* fix raw sql with ISqlSyntaxProvider name escaping

* reduce hard coded strings

* Fix Raw Sql in MemberFilterRepository

* fix formating

* restore MemberFilterRepository

* Correct usage of field name constant.

---------

Co-authored-by: Andy Butland <abutland73@gmail.com>
2026-04-28 08:55:42 +00:00
Niels Lyngsø b4e4a6db25 apply entity-type to the workspace data-mark 2026-04-28 10:32:00 +02:00
Andy ButlandandGitHub b67ee798d5 Integration tests: Tolerate deadlocks in concurrent external login test (#22583)
* Prevent Concurrent_Save_Same_Login_Should_Not_Throw_Duplicate_Key_Exception from failing when exceptions other than what is being guarded against are triggered.

* Addressed code review feedback.
2026-04-28 09:49:22 +02:00
Andreas ZerbstandGitHub 2aa40e7629 E2E: QA: fixed outdated acceptance tests to match frontend changes (#22590)
* Updated helpers

* Updated test
2026-04-28 07:19:16 +00:00
MoleandGitHub fcf5af3d16 Docker Compose template: Improve secrets handling and add script to trust development certificates (#22613)
* Generate random guid for cert pass

* Changes from review

* Move cert generation and add script to trust cert on host machine

* Generate simple hmac key
2026-04-28 10:06:25 +09:00
03cfdb6480 Collection: Add table kind collection view (#22163)
* Add table collection view and manifests

* Use table kind in collection example

* Update entity-name-table-column-layout.element.ts

* Recompute table rows when item hrefs change

* define and render columns from manifest

* wip language implementation

* map to unique field

* rename to label

* test implementation for users table

* clean up

* add example entity actions

* add example description

* Update table-collection-view.element.ts

* Omit base 'meta' and relax table meta type

* Hardcode description column when present

* localize column names

* Update table-collection-view.element.ts

* Type manifest on collection view elements

* Use UmbLitElement instead of LitElement

* fix types

* Update entity-name-table-column-layout.element.ts

* provide entity context for each table row

* fix breaking change and introduce a deprecation warning

* Add status column to example collection view + localize column labels

* implement the UmbTableColumnLayoutElement interface

* add tests for the table collection view

* Make host element optional; add table docs/types

* Update controller-host.mixin.ts

* Update src/Umbraco.Web.UI.Client/src/packages/core/entity-action/global-components/entity-actions-table-column-view/entity-actions-table-column-view.element.ts

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update controller-host.mixin.ts

* Update entity-name-table-column-layout.element.ts

* Update entity-actions-table-column-view.element.ts

* Handle undefined row element in table rendering

Allow onRowRendered to accept an undefined element and clean up row contexts when a row is unmounted. Update the callback signature in table.element.ts and handle the undefined case in table-collection-view.element.ts by destroying the host and removing the stored context for the item to avoid memory leaks when rows are removed.

* Update controller-host.mixin.ts

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Niels Lyngsø <nsl@umbraco.dk>
2026-04-27 20:04:09 +02:00
Andreas Zerbst e4c89092e2 Block Workspace: Add data-mark for acceptance test locator 2026-04-27 13:07:33 +02:00
Niels Lyngsø f292972078 offset condition 2026-04-27 12:27:47 +02:00
Niels Lyngsø cfe5ea4a5f improve switch condition 2026-04-27 12:24:25 +02:00
Niels Lyngsø de01efe718 fix test 2026-04-27 12:24:16 +02:00
Mads Rasmussen c364d0b629 Update base-extension-initializer.controller.ts 2026-04-27 11:12:32 +02:00
Mads Rasmussen 427b32fbd4 move block language access controller to block package 2026-04-27 10:11:31 +02:00
Andy ButlandandGitHub a832090c80 Migrations: Align type attribute casing in locallink migration for integer-based legacy links (closes #22597) (#22599)
* Align GUID-via-UDI and integer locallink sources in migration to consistent type attribute casing.

* Handle Pascal cased type attributes from local links.
2026-04-27 09:31:59 +02:00
Andy ButlandandGitHub d490554458 Public Access: Honour custom IMemberGroupService in backoffice dialog (closes #22580) (#22588)
Use IMemberGroupService for public access group selection and rendering.
2026-04-27 06:48:24 +02:00
Andreas Lykke BorgandGitHub 9c0c301a26 Link picker: Added swedish translations for link picker (closes #22542) (#22596)
Added swedish translations for link picker
2026-04-26 15:09:52 +02:00
cb1bebff91 Variant-Selector: improve visual alignment for segments (#22605)
* improve visual alignment for segments

* remove expand area for segments

Co-authored-by: Copilot <copilot@github.com>

---------

Co-authored-by: Copilot <copilot@github.com>
2026-04-26 14:43:29 +02:00
Andy ButlandandGitHub e6cba5ed09 Health Check: Add check for untrusted database constraints on SQL Server (#22592)
* Add healthcheck for verification of trusted database constraints.

* Re-use the SQL and DTO between the migration and healthcheck.
2026-04-26 09:50:31 +02:00
Andy ButlandandNiels Lyngsø 3de31c4a19 Segments: Preserve segmented property values after save (closes #22166) (#22173)
* Preserve segment-specific property values after save and publish.

* Addressed feedback from code review.

* Moved fix to a projection in UmbPropertyValuePresetVariantBuilderController.

---------

Co-authored-by: Niels Lyngsø <nsl@umbraco.dk>
2026-04-26 09:44:26 +02:00
a056da9c85 Segments: Preserve segmented property values after save (closes #22166) (#22173)
* Preserve segment-specific property values after save and publish.

* Addressed feedback from code review.

* Moved fix to a projection in UmbPropertyValuePresetVariantBuilderController.

---------

Co-authored-by: Niels Lyngsø <nsl@umbraco.dk>
2026-04-24 21:23:59 +00:00
Niels Lyngsø 0f2ffb96a8 more variantId tests 2026-04-24 20:28:21 +02:00
Niels Lyngsø d6e5ff11d0 more guard unit tests 2026-04-24 20:22:12 +02:00
Niels Lyngsø 2415d72651 unit test for reactive fallback feature 2026-04-24 19:38:27 +02:00
Niels Lyngsø 831593c740 remove as const 2026-04-24 19:08:38 +02:00
Niels Lyngsø 83dd3e258e mark as readonly and make js-const 2026-04-24 19:08:03 +02:00
Niels Lyngsø c8b08f76ab remove style import 2026-04-24 19:07:54 +02:00
Niels Lyngsø 41a6bf3c83 add comment for clarification 2026-04-24 19:07:46 +02:00
Niels Lyngsø ca73e81b3c revert removal of || this._isReadOnly check for component rendering 2026-04-24 18:47:11 +02:00
Niels Lyngsø 03a63364ef prevent cancelled context get to cause problems 2026-04-24 17:25:11 +02:00
Niels Lyngsø d127289031 observe fallback for property + name guards 2026-04-24 17:02:32 +02:00
Niels Lyngsø 0c55587c7e no if sentence 2026-04-24 17:02:10 +02:00
Niels Lyngsø 31dfd52b72 todo comments for future 2026-04-24 16:52:02 +02:00
Niels Lyngsø ba80e12f6c observe readOnly languages 2026-04-24 16:51:19 +02:00
Niels Lyngsø a2187801ce make guard fallback reactive 2026-04-24 16:51:04 +02:00
Niels Lyngsø 7de9a853c0 read-only tag for Block Workspace 2026-04-24 16:05:21 +02:00
4596b36ab0 Member surface controllers: Add XML documentation and unit test coverage (#22584)
* Add XML header comments and unit tests for member operation surface controllers.

* Addressed code review feedback.

* Further code review feedback.

---------

Co-authored-by: Laura Neto <12862535+lauraneto@users.noreply.github.com>
2026-04-24 13:44:00 +00:00
Niels Lyngsø cccfc33977 inherit readOnly state when block workspace is invariant 2026-04-24 15:13:36 +02:00
Niels Lyngsø 9dcc2e9c15 set fallback on readOnly 2026-04-24 14:57:15 +02:00
Niels Lyngsø dee32a4171 RTE: set manager readOnly 2026-04-24 14:56:53 +02:00
56cb682c99 Add a constant for the "unroutable content" route (#22593)
* Add a constant for the "unroutable content" route

* Add one more constant for URL provider exceptions

* Update src/Umbraco.Core/Routing/UrlProviderExtensions.cs

Co-authored-by: Andy Butland <abutland73@gmail.com>

* Update src/Umbraco.Core/Constants-Routing.cs

Co-authored-by: Andy Butland <abutland73@gmail.com>

* Update src/Umbraco.Core/DeliveryApi/ApiContentRouteBuilder.cs

Co-authored-by: Andy Butland <abutland73@gmail.com>

---------

Co-authored-by: Andy Butland <abutland73@gmail.com>
2026-04-24 12:53:10 +00:00
Niels Lyngsø fbd6c61225 rename file in manifest 2026-04-24 13:19:24 +02:00
Niels Lyngsø c5425fe641 Revert "transform access context into local controller"
This reverts commit 1a83d9586b.
2026-04-24 13:18:09 +02:00
Niels Lyngsø 409c098acd strict compare on config object level, to cover multiple conditions of the same alias. 2026-04-24 11:39:00 +02:00
Niels Lyngsø 570597b78e update js docs 2026-04-24 11:37:28 +02:00
Niels Lyngsø 443b50b2eb simplify match 2026-04-24 11:36:19 +02:00
Niels Lyngsø 7b613a35fc re-introduce submit create button 2026-04-24 11:35:21 +02:00
c8ed4c1d3f Handle "broken" ancestor publish path in legacy routing (#22586)
* Handle "broken" ancestor publish path in legacy routing

* Update tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentUrlServiceTests.cs

Co-authored-by: Andy Butland <abutland73@gmail.com>

* Additional tests to validate handling of broken publish ancestor chain for invariant content

---------

Co-authored-by: Andy Butland <abutland73@gmail.com>
2026-04-24 10:54:41 +02:00
Niels Lyngsø 1a83d9586b transform access context into local controller 2026-04-23 20:43:58 +02:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>Jacob Overgaard
51efbac3ea Bump the npm_and_yarn group across 3 directories with 3 updates (#22578)
* Bump the npm_and_yarn group across 3 directories with 3 updates

Bumps the npm_and_yarn group with 1 update in the /src/Umbraco.Web.UI.Client directory: [uuid](https://github.com/uuidjs/uuid).
Bumps the npm_and_yarn group with 1 update in the /src/Umbraco.Web.UI.Client/src/packages/core directory: [uuid](https://github.com/uuidjs/uuid).
Bumps the npm_and_yarn group with 2 updates in the /src/Umbraco.Web.UI.Login directory: [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) and [handlebars](https://github.com/handlebars-lang/handlebars.js).


Updates `uuid` from 13.0.0 to 14.0.0
- [Release notes](https://github.com/uuidjs/uuid/releases)
- [Changelog](https://github.com/uuidjs/uuid/blob/main/CHANGELOG.md)
- [Commits](https://github.com/uuidjs/uuid/compare/v13.0.0...v14.0.0)

Updates `uuid` from 13.0.0 to 14.0.0
- [Release notes](https://github.com/uuidjs/uuid/releases)
- [Changelog](https://github.com/uuidjs/uuid/blob/main/CHANGELOG.md)
- [Commits](https://github.com/uuidjs/uuid/compare/v13.0.0...v14.0.0)

Updates `vite` from 7.3.1 to 7.3.2
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/v7.3.2/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v7.3.2/packages/vite)

Removes `handlebars`

---
updated-dependencies:
- dependency-name: uuid
  dependency-version: 14.0.0
  dependency-type: direct:production
  dependency-group: npm_and_yarn
- dependency-name: uuid
  dependency-version: 14.0.0
  dependency-type: direct:production
  dependency-group: npm_and_yarn
- dependency-name: vite
  dependency-version: 7.3.2
  dependency-type: direct:development
  dependency-group: npm_and_yarn
- dependency-name: handlebars
  dependency-version: 
  dependency-type: indirect
  dependency-group: npm_and_yarn
...

Signed-off-by: dependabot[bot] <support@github.com>

* deps: pin @hey-api/openapi-ts to specific version for Login

* deps: use latest Vite on the v7 line to avoid breaking runtime changes

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com>
2026-04-23 13:37:02 +00:00
Jacob Overgaard 91837ebd4d Merge branch 'release/17.4.0' of https://github.com/umbraco/Umbraco-CMS into release/17.4.0 2026-04-23 11:29:17 +02:00
Jacob Overgaard dbcc982251 Merge remote-tracking branch 'origin/main' into release/17.4.0 2026-04-23 11:28:08 +02:00
d911505a00 Slider: Add minimumRange configuration for range sliders (partially closes #22067) (#22078)
* Definition and validation of minimum range for slide property editor.

* Address code review feedback.

* Treat an incorrectly configured negative minimum range as zero.

---------

Co-authored-by: Niels Lyngsø <nsl@umbraco.dk>
2026-04-23 11:27:50 +02:00
Andy Butland 86176f7461 Subscriber Server Role: Skip URL/alias persistence on subscribers with read-only databases (closes #22570) (#22572)
* Ensure that DocumentUrlService and DocumentUrlAliasService will respect read-only, subscriber databases.

* Fixed breaking change in constructor.

* Clarified comment.

* Use pattern matching in SkipDatabaseWrites() check.
2026-04-23 11:08:01 +02:00
Andy ButlandandGitHub 107cfbf9f6 Subscriber Server Role: Skip URL/alias persistence on subscribers with read-only databases (closes #22570) (#22572)
* Ensure that DocumentUrlService and DocumentUrlAliasService will respect read-only, subscriber databases.

* Fixed breaking change in constructor.

* Clarified comment.

* Use pattern matching in SkipDatabaseWrites() check.
2026-04-23 11:02:19 +02:00
Jacob Overgaard dffd60edf6 set version to 17.4.0-rc 2026-04-23 10:30:44 +02:00
Jacob Overgaard 3ab9d7c492 Merge remote-tracking branch 'origin/main' into release/17.4.0 2026-04-23 10:28:55 +02:00
Andreas ZerbstandGitHub f1eaf604e8 Nightly Pipeline: Skip E2E and Integration stages when Build fails (#22568)
Updated dependsOn so the tests dont run if build failed/cancelled
2026-04-23 12:27:48 +07:00
Andy Butland 1045ad7ae2 Document URL Aliases: De-duplicate repeated aliases to prevent upgrade failure (#22569)
* Ensure DocumentUrlAliasService safely de-duplicates repeated aliases.

* Addressed code review feedback.
2026-04-23 06:46:43 +02:00
Andy Butland 44a42352cb Bump version to 17.5.0-rc. 2026-04-23 06:43:21 +02:00
Andy ButlandandGitHub b70e2ae7bc Document URL Aliases: De-duplicate repeated aliases to prevent upgrade failure (#22569)
* Ensure DocumentUrlAliasService safely de-duplicates repeated aliases.

* Addressed code review feedback.
2026-04-22 23:40:35 +02:00
Niels Lyngsø 52c29e3105 revert logic 2026-04-22 22:36:57 +02:00
Niels LyngsøandGitHub 5117e1ee24 Merge branch 'main' into v17/hotfix/22472 2026-04-22 22:34:23 +02:00
Niels Lyngsø 21dd725bc2 clean up 2026-04-22 22:31:07 +02:00
Niels Lyngsø 2811758e3f clean up 2026-04-22 22:29:02 +02:00
Niels Lyngsø 3878bb2009 unit test for the actual problem 2026-04-22 22:27:51 +02:00
Niels Lyngsø d3526d3448 clean up 2026-04-22 22:27:29 +02:00
Niels Lyngsø e9f85e1569 fix and clean-up 2026-04-22 22:10:51 +02:00
Niels Lyngsø cffac815f1 improve life cycle for extension initializer 2026-04-22 21:38:21 +02:00
2f09fd4ca0 Frontend: Fix umb-table Firefox rendering when columns change (closes #22411) (#22414)
* fix(frontend): use keyed repeat for umb-table columns to fix Firefox rendering (#22411)

Column rendering used .map() without keys, causing Firefox's CSS
table-* layout to break when columns changed after initial render.
Switch to repeat() with column.alias keys so Lit properly inserts/removes
DOM nodes. Also removes a stray </uui-table-cell> closing tag.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(frontend): wrap umb-table in Lit `keyed` so Firefox rebuilds the table when columns change

The `repeat()` + alias key change alone did not fix the Firefox issue: Firefox's
`display: table-*` layout engine fails to relayout when cells are inserted into
existing rows, even when Lit's keyed reconciliation does the right thing.

Wrap the `<uui-table>` render in `keyed(columnKey, ...)` so that whenever the
column set changes (keyed on the joined column aliases), Lit discards the entire
subtree and builds a fresh one. Firefox then paints a brand-new table and its
buggy incremental relayout path never runs.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* docs(frontend): document UmbTableColumn.alias uniqueness constraint

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(frontend): reattach sorter on column rebuild and harden column key

Address two review comments on the keyed() rebuild:

- UmbSorterController caches its container element on first
  initialization, so when keyed() replaces <uui-table> the sorter stays
  attached to the detached node. Toggle disable()/enable() in updated()
  when the column signature changes and the table is sortable, so the
  sorter reattaches to the fresh table.
- Build the column key via JSON.stringify instead of a pipe-joined
  string, so aliases containing '|' can't collide and defeat the rebuild.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-22 17:53:46 +01:00
Andy ButlandandGitHub 1725de6a9c Decimal: Allow decimal values when step size is not configured (closes #22127) (#22128) 2026-04-22 18:11:10 +02:00
1a316255c8 Document Management: Clear per-culture published flags when copying a document (closes #22540) (#22567)
* Content: Clear per-culture published flags when copying a document (closes #22540)

When copying a published culture-variant document, the document-level
published flag was cleared on the copy, but the per-culture published
info (mapped to umbracoDocumentCultureVariation.published) was carried
over from the source. This left the database in an inconsistent state
where the document was unpublished overall but each culture row
reported published=1.

Clear PublishCultureInfos on both the root copy and its descendants
alongside the existing Published=false assignment so no culture
variations are persisted as published on the copy.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* Address review feedback: use ClearPublishInfos() helper + add recursive test

- Replace direct property assignment with the existing ClearPublishInfos()
  extension method for semantic clarity and consistency with UnpublishCulture.
- Rename test to match the Can_Copy_* convention used by neighbouring tests.
- Add a second test that exercises the recursive descendant path, confirming
  per-culture published flags are also cleared on descendants.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* Updates integration tests to explicitly verify the fix.

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: Andy Butland <abutland73@gmail.com>
2026-04-22 15:43:08 +00:00
Niels Lyngsø 8f1d4c49fc revert 2026-04-22 17:37:14 +02:00
Niels Lyngsø 2ff73f55a4 make isPermittedForObservableVariant return undefined in bad case 2026-04-22 17:36:03 +02:00
Niels Lyngsø 4679d9df77 simplify document-block-property-level-permissions 2026-04-22 17:35:14 +02:00
Andy ButlandandGitHub 183c85e560 Permissions: Route UI permission retrieval through IContentPermissionService (closes #22351) (#22400)
* Route UI permission retrieval through IContentPermissionService.

* Addressed code review feedback.

* Update OpenApi.json and client-side types.
2026-04-22 11:09:02 +00:00
1792cfe6f2 Surface controllers: validate redirect url in public surface controllers (#22561)
* fix: prevent open redirect in public surface controllers by validating RedirectUrl with Url.IsLocalUrl

* Update src/Umbraco.Web.Website/Controllers/UmbLoginStatusController.cs

Co-authored-by: Andy Butland <abutland73@gmail.com>

* Update src/Umbraco.Web.Website/Controllers/UmbProfileController.cs

Co-authored-by: Andy Butland <abutland73@gmail.com>

* Update src/Umbraco.Web.Website/Controllers/UmbRegisterController.cs

Co-authored-by: Andy Butland <abutland73@gmail.com>

---------

Co-authored-by: Lan Nguyen Thuy <lnt@umbraco.dk>
Co-authored-by: Andy Butland <abutland73@gmail.com>
2026-04-22 10:44:54 +00:00
Niels Lyngsø 2cb015f42b Merge branch 'main' into v17/hotfix/22472 2026-04-22 12:32:21 +02:00
Niels Lyngsø 651574db44 setup read only state based on user permissions 2026-04-22 12:31:50 +02:00
Niels Lyngsø e49387cb35 no need for async 2026-04-22 12:31:26 +02:00
Niels Lyngsø 7351409b35 stop inheriting read only 2026-04-22 12:31:13 +02:00
Andy ButlandandGitHub 28fb93f792 Backoffice: Stop UI filtering invariant document URLs by display culture (closes #22556) (#22560)
* Avoid UI filtering invariant document URLs by display culture.

* Clarified comments.
2026-04-22 11:58:29 +02:00
9adf5307e3 Security: Prevent XXE opportunity in OEmbedProviderBase (#22550)
* test(OEmbedProviderSecurityTests): Tests for permissive DtdProcessing (CA3075)

* fix(OEmbedProviderBase): Update GetXmlResponseAsync to prevent overly-permissive DtdProcessing

https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/quality-rules/ca3075

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* refactor(OEmbed,-OEmbedTests): close string reader inputs, linting, remove check for "DTD" in exception message

* Update src/Umbraco.Core/Media/EmbedProviders/OEmbedProviderBase.cs

Co-authored-by: Andy Butland <abutland73@gmail.com>

---------

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Andy Butland <abutland73@gmail.com>
2026-04-22 08:44:26 +00:00
65b85fd5d2 Relations: Swallow exceptions when retrieving references from incompatible property values (closes #22197) (#22207)
* Correct logging and swallowing of exceptions when retrieving references with changed property types.

* Addressed code review feedback.

* Change multi URL picker to fall back to returning an empty collection if the links JSON could not be deserialised.

---------

Co-authored-by: Laura Neto <12862535+lauraneto@users.noreply.github.com>
2026-04-22 10:31:56 +02:00
Andy ButlandandGitHub 00f0d2340e Cache: Gracefully handle inconsistent published version state (closes #22293) (#22296)
* Defensively handle case where published status in databse is corrupt.

* Addressed code review feedback.

* Further code review feedback.

* Similar fix for NRE in rebuild of document URLs.
2026-04-22 09:31:00 +02:00
Niels Lyngsø 49f8aab7ae parse readonly state, without variant ids as origin is the property read-only state 2026-04-22 08:16:26 +02:00
Niels Lyngsø dc2b471e4c INVARIANT variant id as static 2026-04-22 08:15:46 +02:00
Andy ButlandandGitHub 518adf51b0 Cache: Add deferred content type rebuild mode with de-duplication (#22194)
* Add option for rebuild following content type update in the background.

* Add integration test for deferred rebuild.

* Addressed code review feedback.

* add retry and graceful shutdown to deferred cache rebuild.

* Prevent shared DB connection in deferred rebuild background task.

* Move deferred rebuild trigger to post-scope notification.

* Introduce similar deferred behaviour for Examine reindexing.

* Prevent background cache rebuild from blocking foreground content saves.

* Handle potential case of primary key constraint violation when deferred rebuilding content cache and a content item is saved.

* Improved variable naming.
2026-04-22 07:55:12 +02:00
Andy ButlandandGitHub ef1f760847 Migrations: Fix Label long-string data type dbType (closes #22553) (#22557)
* Add migration to fix data type storage for labels configured with a long string value type.

* Fixed class name and added additional test from code review feedback.

* Further code review feedback.

* Add further test.
2026-04-22 12:46:00 +09:00
Andy ButlandandGitHub 5b3ab2ea2a Published Content Cache: Defensive hardening against race conditions (closes #22254, #22384) (#22393)
* Defensive checks against published content being cached as unavailable.

* Addressed code review feedback.

* Make field readonly.
2026-04-22 09:40:26 +09:00
Andy ButlandandGitHub 9dc49df369 Migrations: Optimise sortable value population for date properties (#22547)
* Optimise the populate sortable column migration.

* Further optimisation from code review feedback.
2026-04-22 09:17:34 +09:00
70d1a05a4e EF Core Scoping: Allow separate database connections for custom DbContexts (closes #22131) (#22133)
* Support separate database DbContexts in AddUmbracoDbContext.

* update internal callers to use new non-obsolete AddUmbracoDbContext overload

- UmbracoEFCoreComposer now calls the new overload with explicit shareUmbracoConnection: true
- Add #pragma CS0618 suppression for v18-obsolete overloads delegating to v19-obsolete overloads

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Update further internal caller to use non-obsolete method.

* Addressed code review feedback.

* Updates after merge/final local review.

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-21 13:18:41 +00:00
Andy ButlandandGitHub 818019dc22 Migrations: Fix local link migration losing fragments and query strings (closes #22152) (#22153)
* Correct handling of querystring and anchors in rich text local links.

* Re-organised test class.

* Address code review comments.
2026-04-21 21:22:43 +09:00
Niels Lyngsø f67135a30f keep rendering edit in read-only mode 2026-04-21 13:52:51 +02:00
Andy ButlandandGitHub a42cdc6656 Members: Fix SQL error when combining member type and group filters on filter endpoint (#22209)
Fix member repository filter query construction to support filter by member type and group.
2026-04-21 13:32:12 +02:00
Andy ButlandandGitHub c64f431a23 Performance: Optimize FullDataSetRepositoryCachePolicy usage across all repositories (#22264)
* Optimize ContentTypeRepository to avoid unnecessary deep-cloning on cache reads.

* Used lightweight benchmark and addressed code review comments.

* Optimize TemplateRepository to avoid unnecessary deep-cloning on cache reads.

* Optimize DomainRepository to avoid unnecessary deep-cloning on cache reads.

* Optimize remaining repositories to avoid unnecessary deep-cloning on cache reads.
2026-04-21 13:23:23 +02:00
94336588af Users: Show success dialog after creating API user (closes #21921) (#22426)
* Present dialog for further action after creating an API user.

* Addressed code review feedback.

---------

Co-authored-by: Engiber Lozada <89547469+engijlr@users.noreply.github.com>
2026-04-21 11:11:47 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>Jacob Overgaard
d78eb98109 Bump the npm_and_yarn group across 3 directories with 4 updates (#22537)
* Bump the npm_and_yarn group across 3 directories with 4 updates

Bumps the npm_and_yarn group with 1 update in the /src/Umbraco.Web.UI.Client directory: [basic-ftp](https://github.com/patrickjuchli/basic-ftp).
Bumps the npm_and_yarn group with 2 updates in the /src/Umbraco.Web.UI.Login directory: [picomatch](https://github.com/micromatch/picomatch) and [handlebars](https://github.com/handlebars-lang/handlebars.js).
Bumps the npm_and_yarn group with 1 update in the /tests/Umbraco.Tests.AcceptanceTest directory: [lodash](https://github.com/lodash/lodash).


Updates `basic-ftp` from 5.2.2 to 5.3.0
- [Release notes](https://github.com/patrickjuchli/basic-ftp/releases)
- [Changelog](https://github.com/patrickjuchli/basic-ftp/blob/master/CHANGELOG.md)
- [Commits](https://github.com/patrickjuchli/basic-ftp/compare/v5.2.2...v5.3.0)

Updates `picomatch` from 4.0.3 to 4.0.4
- [Release notes](https://github.com/micromatch/picomatch/releases)
- [Changelog](https://github.com/micromatch/picomatch/blob/master/CHANGELOG.md)
- [Commits](https://github.com/micromatch/picomatch/compare/4.0.3...4.0.4)

Removes `handlebars`

Updates `picomatch` from 4.0.3 to 4.0.4
- [Release notes](https://github.com/micromatch/picomatch/releases)
- [Changelog](https://github.com/micromatch/picomatch/blob/master/CHANGELOG.md)
- [Commits](https://github.com/micromatch/picomatch/compare/4.0.3...4.0.4)

Updates `lodash` from 4.17.23 to 4.18.1
- [Release notes](https://github.com/lodash/lodash/releases)
- [Commits](https://github.com/lodash/lodash/compare/4.17.23...4.18.1)

Updates `lodash` from 4.17.23 to 4.18.1
- [Release notes](https://github.com/lodash/lodash/releases)
- [Commits](https://github.com/lodash/lodash/compare/4.17.23...4.18.1)

Updates `lodash` from 4.17.23 to 4.18.1
- [Release notes](https://github.com/lodash/lodash/releases)
- [Commits](https://github.com/lodash/lodash/compare/4.17.23...4.18.1)

---
updated-dependencies:
- dependency-name: basic-ftp
  dependency-version: 5.3.0
  dependency-type: indirect
  dependency-group: npm_and_yarn
- dependency-name: picomatch
  dependency-version: 4.0.4
  dependency-type: indirect
  dependency-group: npm_and_yarn
- dependency-name: handlebars
  dependency-version: 
  dependency-type: indirect
  dependency-group: npm_and_yarn
- dependency-name: picomatch
  dependency-version: 4.0.4
  dependency-type: indirect
  dependency-group: npm_and_yarn
- dependency-name: lodash
  dependency-version: 4.18.1
  dependency-type: indirect
  dependency-group: npm_and_yarn
- dependency-name: lodash
  dependency-version: 4.18.1
  dependency-type: indirect
  dependency-group: npm_and_yarn
- dependency-name: lodash
  dependency-version: 4.18.1
  dependency-type: indirect
  dependency-group: npm_and_yarn
...

Signed-off-by: dependabot[bot] <support@github.com>

* build(deps-dev): bumps @hey-api/openapi-ts to 0.85.2 for everything

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com>
2026-04-21 11:02:25 +00:00
Andy ButlandandGitHub dc1ac9fb8d Boot Failed: Add missing BootFailed.html error page (closes #17144) (#22120)
* Added missing "boot failed" page and adjust gitignore to include in repository.

* Adjust base path to support virtual directory hosting.
2026-04-21 19:47:13 +09:00
Jacob Overgaard 8484bab495 Issue Deduplication: Fix tool name and add manual dispatch
The allowlist referenced mcp__github__create_issue_comment, which
doesn't exist in github-mcp-server v0.17.1 (the tool is
add_issue_comment). Claude's attempts to comment were denied, so
duplicates were labelled but no explanation comment was posted.

Also adds a workflow_dispatch trigger with an issue_number input and
enables show_full_output so future denials are visible in logs.
2026-04-21 11:49:05 +02:00
LLavertyandJacob Overgaard 3de27358d5 docs(security.md): Update Sanitize HTML documentation to prefer the umbraco-cms interface instead of DOMPurify 2026-04-21 11:37:27 +02:00
Niels LyngsøandGitHub d3d0e40fd3 Eslint: Rule for Manifest Aliases (#22316)
* eslint rule for Manifest Aliases

* update to handle propertyEditorSchema aliases

* make typescript check

* support localization alias

* Make consts for theme manifests

* no rules for themes

* fix not used, double media-type-root manifest, clean up.

* Improve pascal cases test
2026-04-21 11:32:35 +02:00
Andy ButlandandGitHub 25941fd749 Members: Add lightweight external-only members (closes #12741) (#22162)
* Models, service, repository and migration for external members.

* Integrate identity for external members in MemberUserStore.

* When autolinking external member, skip member type.

* Populate profile.

* Revoke member tokens for delivery API for external members.

* Audit notification handling.

* Management API updates for external members.

* Added IMemberFilterService for combined member queries from management API.

* Referenced by member controller with external members.

* Guard password reset for external members.

* Remove ExternalMemberSettings.

* Convert between content and external members.

* Fixed ambiguous constructor.

* Update OpenApi.json.

* Update client SDK.

* Backoffice ui for external members.

* Refactor member collection retrievel to use presentation factory.
Fixes in testing.

* Fixes from testing.

* Fix icon display on member picker.

* Add external member support to member picker value converter.

* Delete fix, sync data fix, Examine indexing, member collection default icon.

* Add cache refreshers for external members.

* Remove unused "fast path" for just updating login properties.

* Addresed code review feedback.

* Further integration tests.

* Fixed failing unit test.

* Update typed client.

* Addressed code review feedback.

* Early return to reduce nesting in ReferencedByMemberController.

* Introduce MemberPresentationService and MemberReferenceService to move logic out of controllers.

* Test for and fix SQLite deadlock related to cross-store uniqueness checks.

* Additional fix for the "content" member creation.

* Defer external member Examine indexing via the background task queue.

* Add update date to external member record (aligning with content members).

* Add TreatLoginAsMemberUpdate config so member re-index can be skipped on login.

* Add logging to help verify the indexing path chosen on login and register.

* Move ExternalMemberService into Core to align with MemberService.

* Fix deserialization issue with Json payloads.

* Display of external member profile data in backoffice.

* Fixed breaking change.

* Consider existing behaviour of bumping update date on login to be a bug, so no need for configuration and backward compatibility efforts.
2026-04-21 11:28:10 +02:00
a6e6585d42 Management API: Reduce user start node tree filtering code duplication (#22486)
* Reduce user start node tree filtering code duplication

Extract shared start node filtering logic from UserStartNodeTreeControllerBase
into a dedicated service hierarchy (IUserStartNodeTreeFilterService and
domain-specific implementations for documents and media).

Existing constructor signatures and protected members are preserved as
obsolete to maintain backward compatibility for external consumers.

* Disambiguate DI constructor resolution for tree controllers

Adds obsolete constructors accepting both the legacy dependencies and the new IDocument/IMediaStartNodeTreeFilterService to the eight concrete tree controllers and to MediaTreeControllerBase. These serve as a superset constructor that lets the DI container unambiguously resolve a single constructor, since the new and existing obsolete constructors have non-subset parameter sets and [ActivatorUtilitiesConstructor] is not honoured by CallSiteFactory at ServiceProvider validation time.

* Address review feedback

- Change constructors on DocumentStartNodeTreeFilterService and
  MediaStartNodeTreeFilterService from public to internal (classes are
  already internal).
- Add [EditorBrowsable(Never)] to the disambiguation constructors so
  IDEs hide them from autocomplete.
- Add inline comments explaining the empty-array fallback in the
  obsolete GetUserStartNodeIds/GetUserStartNodePaths overrides.

* Revert filter service constructors to public

DI container requires public constructors for activation, even on
internal classes. Reverts the internal change from the previous commit.

* Add unit tests for UserStartNodeTreeFilterService

Tests ShouldBypassStartNodeFiltering (root access, data type ignore,
no access), MapWithAccessFiltering (access/no-access/missing entities),
and delegation to IUserStartNodeEntitiesService for root, child and
sibling filtering including mixed access scenarios.

* Simplify obsolete-ctor path on document and media tree controllers (#22546)

---------

Co-authored-by: Andy Butland <abutland73@gmail.com>
2026-04-21 11:26:52 +02:00
Niels LyngsøandGitHub 0afa6f30fe Block Editor: Create Modal Size Overwrite (#22386)
implement data-type config for block catagloue modal size
2026-04-21 11:17:36 +02:00
25d382b1c0 Removed line clamp for data type picker (closes #22515) (#22526)
* Removed line clamp for data type picker

* Removed line clamp on additional labels

---------

Co-authored-by: Engiber Lozada <89547469+engijlr@users.noreply.github.com>
2026-04-21 10:55:31 +02:00
e4e6e04091 User Groups: Add ability to manage users directly from the group workspace (#22215)
* add users section into user group

* fix test failed

* fix unchange issue

* add notification

* add remainging count

* update take 100

* split user list into separate element

* add localization for text

* add repository for user list in user group

* update key message

* remove remainingCount from user-input

---------

Co-authored-by: Lan Nguyen Thuy <lnt@umbraco.dk>
Co-authored-by: Andy Butland <abutland73@gmail.com>
2026-04-21 09:45:25 +01:00
Andreas ZerbstandGitHub 63fe8cfd55 E2E: QA: Added acceptance tests for member authentication (#22466)
* Added early steps of member auth

* Cleaned up

* Cleaned up again

* Cleaned up

* Fixes based on comments

* Updated name of helper

* Reverted to old smokeTest command
2026-04-21 08:17:12 +00:00
Andy ButlandandGitHub c507f43912 Relations: Fire relation notifications for automatic relations (closes #22222) (#22345)
* Emit relation saved and deleted notification when automatic relations are added and removed during content updates.

* Addressed code review feedback.
2026-04-21 10:16:25 +02:00
4fc3a56c8f V17/media notification (#22484)
* swapping from column to row

* adds same look for when you upload image on a content node

* Remove duplicated css property

---------

Co-authored-by: engjlr <enl@umbraco.dk>
Co-authored-by: Engiber Lozada <89547469+engijlr@users.noreply.github.com>
2026-04-21 08:56:50 +02:00
d58d5b246b User Service: Remove IBackOfficeUserStore service location from read methods (closes #22404) (#22408)
* Avoid requirement for IBackOfficeStore registrations for non-backoffice configured setups.

* Apply same update to other read method potentially called from non backoffice setups.

* Remove comment.

* Preserve GetUserById upgrade fallback; strengthen test assertions

- Add IRuntimeState to UserService and mirror the DbException catch
  from BackOfficeUserStore.GetAsync(int) in GetUserById, so the
  upgrade-time fallback to GetForUpgrade is preserved.
- Use non-empty arguments in the delivery-only integration test so
  the repository-backed code paths are actually exercised, not just
  the early-return guards.
- Update UserServiceCrudTests to pass IRuntimeState to the new
  constructor parameter.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Introduce IBackOfficeUserReader to avoid code duplication for user read methods between UserService and BackOfficeUserStore.

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-21 08:56:31 +02:00
ed8246390c Authorization: Fix publish with descendants returning 403 with granular permissions (closes #22140) (#22148)
* Fix branch authorization from requiring recycle bin permission.

* Use named parameters.

---------

Co-authored-by: Laura Neto <12862535+lauraneto@users.noreply.github.com>
2026-04-20 13:46:19 +00:00
46c402dda9 Upgrade Screen: Detect and display correct "from" version (closes #20980) (#22387)
* fix(api): resolve correct old version on upgrade screen (closes #20980)

The upgrade screen always showed the first version of the current major
(e.g. 17.0.0) regardless of the actual database state. This was because
UpgradeSettingsFactory constructed OldVersion from just the running
app's major version number.

The fix adds UmbracoPlan.GetVersionForState() which walks the migration
transition chain and extracts version numbers from migration type
namespaces (V_{major}_{minor}_{patch} convention). RuntimeState calls
this during startup and exposes the result via a new
IRuntimeState.CurrentMigrationVersion property (with a default null
implementation to avoid breaking changes). UpgradeSettingsFactory uses
this resolved version with a fallback to the previous behaviour.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(api): return 9.4.0 for InitialState in GetVersionForState

InitialState is the final migration state of 9.4 (the lowest supported
upgrade). Returning null caused the fallback to show <major>.0.0 for
databases at that state. Now correctly resolves to 9.4.0.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* chore(infrastructure): add TODO (V18) to update initialVersion when InitialState changes

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Added TODO for 18.

* Addressed code review feedback.

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-20 14:25:58 +02:00
Andy ButlandandGitHub 27263c56ea Migrations: Await EF Core premigrations for OpenIddict (closes #22200) (#22205)
Await EF Core premigrations for OpenIddict.
2026-04-20 20:40:52 +09:00
Andy ButlandandGitHub afd885f358 Developer Tools: Add umb-bump-version skill for automating version bumps (#22438)
* Adds a bump version skill.

* Amends from code review.

* Further updates from code review.
2026-04-20 12:53:47 +02:00
Andy ButlandandGitHub 307e5d5c1b Backoffice Identity: Add Override method to IBackOfficeSecurityAccessor for background processing (#22499)
* Allow packages and hosted services to set an ambient backoffice identity via AsyncLocal for scenarios where no HttpContext is available.

* Addressed code review feedback.
2026-04-20 12:41:38 +02:00
0248dcc020 Performance: Avoid allocating a string if _publishedContentCache has a cached version in MediaCacheService. (#22535)
* Avoid allocating a string if _publishedContentCache has a cached version & removed preview param, it was always false

* Clarified comment, used GetCacheKey method from location where string was being created.

---------

Co-authored-by: Andy Butland <abutland73@gmail.com>
2026-04-20 10:03:44 +00:00
a9a370c357 Performance: Use GeneratedRegex instead of generating at runtime in string extensions (#22534)
* Use GeneratedRegex instead of generating at runtime

* Add unit tests to verify refactored code.

---------

Co-authored-by: Andy Butland <abutland73@gmail.com>
2026-04-20 11:31:43 +02:00
Andy ButlandandGitHub 92c5d3f6ed Templating: Correct the updated Navigation snippet (closes #22528) (#22530)
* Corrects the navigation snippet.

* Treat Model as required in Navigation snippet.
2026-04-20 11:17:15 +02:00
927eacf5c1 Update npm dependencies for v17.4.0-rc (#22464)
* update npm dependencies for v17.4.0 minor release

* update dependencies package

* fix lint errors

* remove Dribbble from lucide to simple icons

* revert @hey-api/openapi-ts bump

* chore: regenerate sdk.gen.ts

* chore: regenerate msw sw

* chore: regenerate icons

* build: excludes "mocks/tools" from being compiled

it is an isolated project and so can be used independent of the backoffice

---------

Co-authored-by: Lan Nguyen Thuy <lnt@umbraco.dk>
Co-authored-by: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com>
2026-04-20 09:06:31 +00:00
Andy ButlandandGitHub bbf5760c2d Trees: Respect 'Ignore user start nodes' on expand (closes #22487) (#22510)
* Propagate tree context's additional request args to tree item children, ensuring tree item children respect the "ignore user start nodes" data type setting for content pickers.

* Add unit tests for additional request args forwarding to tree item children manager.
2026-04-20 10:55:21 +02:00
Andy ButlandandGitHub fc87b3efda Documents: Present blueprint options from collection view Create button (closes #22529) (#22533)
* Add option to select blueprint when creating a document from a collection view.

* Addressed code review feedback.
2026-04-20 10:27:02 +02:00
Mads RasmussenandClaude Sonnet 4.6 7c7073428d qa(backoffice): add client-side tests for UmbWebhookCollectionRepository
Covers requestCollection with shape validation and pagination behaviour
(take, skip, consistent total) using the kitchen sink mock set.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-17 20:45:55 +02:00
Mads RasmussenandClaude Sonnet 4.6 e79f05e8f5 qa(backoffice): add client-side tests for UmbWebhookDetailRepository
Covers createScaffold, requestByUnique, create, save, and delete using
the kitchen sink mock set and MSW-intercepted webhook endpoints.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-17 20:32:36 +02:00
Mads RasmussenandClaude Sonnet 4.6 e1567d6c20 qa(backoffice): add client-side tests for UmbWebhookItemRepository
Uses the kitchen sink mock set to test requestItems and items against
the MSW-intercepted webhook item endpoint.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-17 19:12:48 +02:00
Niels LyngsøandGitHub a1359abeb9 Icons: extends icon data + improved search (#22436)
* extend icons with information from theseaurus

* implement new icon search logic

* clean-up data

* sorting with a backup of the name

* refactor into a controller

* improve multi word group search

* embed lucide data

* rename tech into technology

* remove paper from dollar
2026-04-17 18:29:52 +02:00
4d27e1972d Backoffice Mocks: Fixes to Kitchen Sink mock data (#22512)
* upgrade msw, migrate all interceptors, and update backoffice integration

* Fix msw test runner integration

* wip mock sets

* align news mock data

* clean up

* add interface for mock sets

* Refactor mock DBs to use dataSet directly

* export as data

* align exports

* Update index.ts

* simplify

* remove createTemplateScaffold from data set

* remove getGroupByName from mock set

* remove getGroupWithResultsByName from mock set

* remove getIndexByName from mock set

* remove unused getSearchResultsMockData function

* Add kenn mock data set with SQLite transformation scripts

Introduces a new "kenn" mock data set generated from an Umbraco SQLite database export.

Transformation scripts (devops/sqlite-to-mock/):
- Database connection helper using sql.js
- Transform scripts for data-types, document-types, media-types, documents, media, users, templates, languages, and dictionary

Generated kenn data set includes:
- 156 data types
- 48 document types with properties, containers, and compositions
- 11 media types
- 41 documents with property values and variants
- 75 media items
- 4 users and 6 user groups
- 10 templates, 2 languages, 5 dictionary items

Usage: VITE_MOCK_SET=kenn npm run dev

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* Fix Tab/Group container type mapping in document types

The PropertyGroupType enum in Umbraco.Core defines:
- Group = 0
- Tab = 1

The transformer had this inverted. Fixed the mapping and regenerated
document-type.data.ts with correct container types.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* Fix user group fallbackPermissions in transformer

Read permissions from umbracoUserGroup2Permission table instead of
the empty userGroupDefaultPermissions column. Also handles mapping
legacy single-letter permission codes to new Umb.Document.* format.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* Remove deprecated createTemplateScaffold from template transformer

This function was removed from the UmbMockDataSet interface.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* Move sqlite-to-mock script to main package.json

Consolidate the SQLite transformation tooling into the main package by:
- Adding sql.js, @types/sql.js, and tsx as devDependencies
- Adding sqlite-to-mock script that runs transformations and lints output
- Removing the separate devops/sqlite-to-mock/package.json and lock file

This allows the transformation scripts to reuse the main node_modules.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* add url manager and url handlers

* Add CLI parameters to sqlite-to-mock script

The script now requires db-path and set-alias arguments:
  npm run sqlite-to-mock -- <db-path> <set-alias>

Changes:
- Add configure(), getDatabase(), getOutputDir(), closeDatabase() for lazy init
- Add CLI argument parsing with validation
- Remove direct execution calls from transform scripts
- Move eslint formatting into the script

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* Generate complete mock data sets and auto-discover sets

- Add generate-supporting-files.ts to create index.ts and all
  placeholder/static files needed for a complete mock data set
- Use import.meta.glob for dynamic mock set discovery instead
  of hardcoded switch statement

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* Add mock handler for document type configuration

Introduces a new mock handler to serve document type configuration data, updates the mock database to provide configuration, and integrates the handler into the document type mock handlers.

* make all mock data optional

* Add custom service worker to bypass static asset requests

Introduces umbServiceWorker.js to intercept and bypass static asset requests before reaching MSW, improving startup performance in Vite development.

* fix type errors

* Update template-query.manager.ts

* change to runtime load of mock data

* Update package-lock.json

* wip mock set switcher

* Use umbMockManager.availableSetNames in mock header

* remove the test set

* clean up

* move mock files to the client project root

* rename folder

* move sqllite tool into mocks folder

* clean up

* rename folder

* update the correct tsconfig file

* Update README.md

* fix path

* mock search

* add mocks for tree siblings endpoint

* add mocks for document type and media type allowed parents

* split user permissions mock data into its own mock set

* Initialize localization registry in date test

* don't use consts. Inits too much from the modules

* Update property-editor-ui-user-picker.test.ts

* Update property-value-cloner-block-grid.cloner.test.ts

* add custom permission

* make test check for custom permission in specific mock set

* use specific mock set with specific user id

* Update document-user-permission.condition.test.ts

* Update section-user-permission.condition.test.ts

* add mock manager util to internal utils

* add import map to test runner

* Move mock-data-set.types and update imports

* Exclude internal consts in export test

* Rename mock key to userPermissions

* Register mock manifests only in development

* manual merge

* Add labels and alias/label list for mock sets

* Add visibility flag for mock sets

* delete kenn mock set

* Update mock-manager.ts

* fix(mocks): correct document type composition generation in sqlite-to-mock

The compositions were reversed - grouped by parentContentTypeId instead
of childContentTypeId. In cmsContentType2ContentType, the parent is the
composed type and the child is the type using it. Also adds inheritance
vs composition detection based on umbracoNode parentId matching.

* Update src/Umbraco.Web.UI.Client/mocks/msw-handlers/member-type/structure.handlers.ts

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update src/Umbraco.Web.UI.Client/mocks/tools/sqlite-to-mock/README.md

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update src/Umbraco.Web.UI.Client/mocks/db/template-detail.manager.ts

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Fix mock DB slice and add safety checks

* Adds "Kitchen Sink" mock data set

* Updates to sqlite-to-mock tool

* Default mock data set tweaks

Replaces "loremflickr.com" images with local placeholders

* "Kitchen Sink" mock data updates

* feat(mocks): add member support to sqlite-to-mock tool

Add transformers for members, member types, and member groups to replace
the empty placeholder arrays. Queries cmsMember, cmsMemberType, and
cmsMember2MemberGroup tables for auth data, property visibility, and
group relationships.

* Updated "Kitchen Sink" mock data with Members

* fix(mocks): type rawData in composition-mapped files to avoid never[] inference

When all compositions arrays are empty, TypeScript infers the element
type as never, causing a type error on the .map() callback. Adding an
explicit type annotation for rawData resolves this. Also fixed in both
generators so future runs produce correctly typed output.

* feat(mocks): implement imaging resize URLs handler

Extract the umbracoFile src from media items and build resize URLs with
width, height, mode, and format query parameters. Replaces the empty
urlInfos placeholder.

* Updated placeholder images

* fix(mocks): return actual media file URLs and add missing folders endpoint

The /media/urls handler was returning ancestor-based slug paths instead
of the umbracoFile source path, causing the image cropper modal to
render a generic file preview instead of an image preview.

Also adds the missing /item/media-type/folders handler that was causing
a crash when opening the media picker.

* fix(mocks): parse JSON values stored in varcharValue column

Short JSON values like Color Picker data are stored in varcharValue
rather than textValue in SQLite. The transformers only attempted
JSON.parse on textValue, leaving varcharValue as raw strings. Now also
parses varcharValue when it starts with { or [.

Also fixes the kitchen-sink Color Picker mock data to use parsed objects.

* fix(mocks): add missing document audit log handler

Adds a handler for GET /document/{id}/audit-log that returns the shared
audit log data from the mock data set. Prevents crash in the document
workspace info view history component.

* Mock data tweaks

* move logic from msw handlers to mock services

* remove debugger

* introduce an audit log db class

---------

Co-authored-by: Mads Rasmussen <madsr@hey.com>
Co-authored-by: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com>
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-04-17 17:05:38 +02:00
Niels LyngsøandGitHub 67f0eb5e4a Fix: parse hashtag strings for confirm dialog localization (#22490)
just parse hashtag strings for confirm dialog localization
2026-04-17 15:03:04 +00:00
Niels Lyngsø 50c5d4eabb remove inheritance of readonly state 2026-04-17 16:36:46 +02:00
Engiber LozadaandGitHub 6f0007df85 Media Picker: Use UUI breadcrumbs to prevent modal overflow with deep folder paths (closes #22286) (#22375)
Use breadcrumbs for media folder path
2026-04-17 14:59:38 +02:00
Jacob OvergaardandGitHub eb217d671a Update model version in issue-deduplication workflow 2026-04-17 14:34:34 +02:00
Andy ButlandandGitHub 722ca0476a Dependencies: Pin System.Security.Cryptography.Xml to resolve vulnerability warning (#22514)
Add direct reference to transitive dependency on System.Security.Cryptography.Xml to ensure we don't depend on a vulnerable version.
2026-04-17 14:27:48 +02:00
94603b6918 adds same drag styling as when dragging item in the content sectin (#22460)
* adds same drag styling as when dragging item in the content sectin

* remove unused loader css

---------

Co-authored-by: Niels Lyngsø <niels.lyngso@gmail.com>
2026-04-17 12:24:00 +00:00
Jacob OvergaardandClaude Opus 4.7 948eefb624 build: allow community-opened issues to trigger dedup workflow
Pass `github_token` and set `allowed_non_write_users: "*"` so the action
bypasses the OIDC actor check, which rejects non-maintainers with
"User does not have write access on this repository". Safe here because
`permissions:` and `--allowedTools` are tightly scoped to issue ops.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-17 14:03:07 +02:00
64e6a7cf8a Backoffice Mocks: Add Webhook Mock Services + Kitchen sink mock data (#22507)
* upgrade msw, migrate all interceptors, and update backoffice integration

* Fix msw test runner integration

* wip mock sets

* align news mock data

* clean up

* add interface for mock sets

* Refactor mock DBs to use dataSet directly

* export as data

* align exports

* Update index.ts

* simplify

* remove createTemplateScaffold from data set

* remove getGroupByName from mock set

* remove getGroupWithResultsByName from mock set

* remove getIndexByName from mock set

* remove unused getSearchResultsMockData function

* Add kenn mock data set with SQLite transformation scripts

Introduces a new "kenn" mock data set generated from an Umbraco SQLite database export.

Transformation scripts (devops/sqlite-to-mock/):
- Database connection helper using sql.js
- Transform scripts for data-types, document-types, media-types, documents, media, users, templates, languages, and dictionary

Generated kenn data set includes:
- 156 data types
- 48 document types with properties, containers, and compositions
- 11 media types
- 41 documents with property values and variants
- 75 media items
- 4 users and 6 user groups
- 10 templates, 2 languages, 5 dictionary items

Usage: VITE_MOCK_SET=kenn npm run dev

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* Fix Tab/Group container type mapping in document types

The PropertyGroupType enum in Umbraco.Core defines:
- Group = 0
- Tab = 1

The transformer had this inverted. Fixed the mapping and regenerated
document-type.data.ts with correct container types.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* Fix user group fallbackPermissions in transformer

Read permissions from umbracoUserGroup2Permission table instead of
the empty userGroupDefaultPermissions column. Also handles mapping
legacy single-letter permission codes to new Umb.Document.* format.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* Remove deprecated createTemplateScaffold from template transformer

This function was removed from the UmbMockDataSet interface.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* Move sqlite-to-mock script to main package.json

Consolidate the SQLite transformation tooling into the main package by:
- Adding sql.js, @types/sql.js, and tsx as devDependencies
- Adding sqlite-to-mock script that runs transformations and lints output
- Removing the separate devops/sqlite-to-mock/package.json and lock file

This allows the transformation scripts to reuse the main node_modules.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* add url manager and url handlers

* Add CLI parameters to sqlite-to-mock script

The script now requires db-path and set-alias arguments:
  npm run sqlite-to-mock -- <db-path> <set-alias>

Changes:
- Add configure(), getDatabase(), getOutputDir(), closeDatabase() for lazy init
- Add CLI argument parsing with validation
- Remove direct execution calls from transform scripts
- Move eslint formatting into the script

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* Generate complete mock data sets and auto-discover sets

- Add generate-supporting-files.ts to create index.ts and all
  placeholder/static files needed for a complete mock data set
- Use import.meta.glob for dynamic mock set discovery instead
  of hardcoded switch statement

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* Add mock handler for document type configuration

Introduces a new mock handler to serve document type configuration data, updates the mock database to provide configuration, and integrates the handler into the document type mock handlers.

* make all mock data optional

* Add custom service worker to bypass static asset requests

Introduces umbServiceWorker.js to intercept and bypass static asset requests before reaching MSW, improving startup performance in Vite development.

* fix type errors

* Update template-query.manager.ts

* change to runtime load of mock data

* Update package-lock.json

* wip mock set switcher

* Use umbMockManager.availableSetNames in mock header

* remove the test set

* clean up

* move mock files to the client project root

* rename folder

* move sqllite tool into mocks folder

* clean up

* rename folder

* update the correct tsconfig file

* Update README.md

* fix path

* mock search

* add mocks for tree siblings endpoint

* add mocks for document type and media type allowed parents

* split user permissions mock data into its own mock set

* Initialize localization registry in date test

* don't use consts. Inits too much from the modules

* Update property-editor-ui-user-picker.test.ts

* Update property-value-cloner-block-grid.cloner.test.ts

* add custom permission

* make test check for custom permission in specific mock set

* use specific mock set with specific user id

* Update document-user-permission.condition.test.ts

* Update section-user-permission.condition.test.ts

* add mock manager util to internal utils

* add import map to test runner

* Move mock-data-set.types and update imports

* Exclude internal consts in export test

* Rename mock key to userPermissions

* Register mock manifests only in development

* manual merge

* Add labels and alias/label list for mock sets

* Add visibility flag for mock sets

* delete kenn mock set

* Update mock-manager.ts

* fix(mocks): correct document type composition generation in sqlite-to-mock

The compositions were reversed - grouped by parentContentTypeId instead
of childContentTypeId. In cmsContentType2ContentType, the parent is the
composed type and the child is the type using it. Also adds inheritance
vs composition detection based on umbracoNode parentId matching.

* Update src/Umbraco.Web.UI.Client/mocks/msw-handlers/member-type/structure.handlers.ts

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update src/Umbraco.Web.UI.Client/mocks/tools/sqlite-to-mock/README.md

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update src/Umbraco.Web.UI.Client/mocks/db/template-detail.manager.ts

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Fix mock DB slice and add safety checks

* webhook mock plan

* init webhook mock set + handlers

* Adds "Kitchen Sink" mock data set

* Updates to sqlite-to-mock tool

* Default mock data set tweaks

Replaces "loremflickr.com" images with local placeholders

* "Kitchen Sink" mock data updates

* Add paginated list and remove collection handler

* feat(mocks): add member support to sqlite-to-mock tool

Add transformers for members, member types, and member groups to replace
the empty placeholder arrays. Queries cmsMember, cmsMemberType, and
cmsMember2MemberGroup tables for auth data, property visibility, and
group relationships.

* Updated "Kitchen Sink" mock data with Members

* fix(mocks): type rawData in composition-mapped files to avoid never[] inference

When all compositions arrays are empty, TypeScript infers the element
type as never, causing a type error on the .map() callback. Adding an
explicit type annotation for rawData resolves this. Also fixed in both
generators so future runs produce correctly typed output.

* Add webhook delivery mock data and handlers

* Add webhook event mock data and handlers

* include webhooks in kitchen sink data set

* Add flags to webhook mock; fix item response

* Support pagination in webhook events handler

* Update src/Umbraco.Web.UI.Client/mocks/db/webhook-delivery.db.ts

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update detail.handlers.ts

* Map webhook event aliases to event objects

* remove note about being created from SQL db

---------

Co-authored-by: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com>
Co-authored-by: leekelleher <leekelleher@gmail.com>
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-04-17 13:31:41 +02:00
a06c4aa4c0 Tiptap RTE: Fix Clear Formatting errors when HTML attribute extensions aren't enabled (closes #22502) (#22509)
* TipTap: Declare Clear Formatting toolbar button's extension dependencies

* Reworked to have a loose dependency

on the `class` and `style` attribute extensions

---------

Co-authored-by: leekelleher <leekelleher@gmail.com>
2026-04-17 10:46:35 +00:00
Jacob Overgaard 13a20c8189 build: disables debug mode for workflow 2026-04-17 12:28:23 +02:00
Jacob Overgaard 4693546e34 build: enables full output to try and see why we run out of credits 2026-04-17 09:39:20 +02:00
Jacob Overgaard 21cfb0b27d build: enables progress tracking to see if/when the job fails 2026-04-17 09:38:43 +02:00
Jacob Overgaard f2f19ba5a7 build: upgrades the actions/checkout task from v4 to v6 (latest) on all workflows 2026-04-17 09:37:23 +02:00
Jacob Overgaard b7a05c005e build: removes "issues: write" permission as this job doesn't need that 2026-04-17 09:35:47 +02:00
Jacob Overgaard 8f72b079c5 build: adds "reopened" state to the claude PR review 2026-04-17 09:35:12 +02:00
Jacob Overgaard 626a78085f build: removes base_branch parameter that is not needed (has the same value as default) 2026-04-17 09:34:18 +02:00
Jacob Overgaard 80127d4647 build: this is firstly to test out Claude and make sure the flow works, but secondly also to try and reduce the number of active issues 2026-04-17 09:33:48 +02:00
a98a6aa390 Performance: Micro-optimisation in UdiParser (eliminate closure, fix naming & formatting of exceptions) (#22506)
* Eliminate closure, fix naming & formatting of exceptions

* Added unit tests around the changed code.

---------

Co-authored-by: Andy Butland <abutland73@gmail.com>
2026-04-17 05:51:36 +00:00
Andy ButlandandGitHub 30977bd0ad Background Jobs: Use ApplicationMainUrl as fallback for absolute URL provision (closes #22420) (#22435)
* Use configured or detected application URL as request URL fallback in background tasks when constructing absolute URLs.

* Addresed code review feedback.
2026-04-17 14:13:27 +09:00
HenrikandGitHub 48a9fb1c38 Code Quality: Use FrozenDictionary and Array instead of Dictionary and List in EntityContainer. (#22505)
Use FrozenDictionary & array instead of Dictionary & List. Fix naming
2026-04-17 07:00:03 +02:00
HenrikandGitHub a4594a3166 Code Quality: Reduce dictionary lookups within lock (#22504)
Reduce dictionary lookups within lock
2026-04-17 06:57:21 +02:00
3b5d95abaa Code Quality/Logging: Fix 'occured' -> 'occurred' typos in log/error/comment strings (#22508)
* Fix occured typo in UserBasedPreviewTokenGenerator.cs

Signed-off-by: SAY-5 <SAY-5@users.noreply.github.com>

* Fix occured typo in IndexPresentationFactory.cs

Signed-off-by: SAY-5 <SAY-5@users.noreply.github.com>

* Fix occured typo in app-error.element.ts

Signed-off-by: SAY-5 <SAY-5@users.noreply.github.com>

* Fix occured typo in UmbracoRouteValueTransformer.cs

Signed-off-by: SAY-5 <SAY-5@users.noreply.github.com>

* Fix occured typo in DocumentUrlFactory.cs

Signed-off-by: SAY-5 <SAY-5@users.noreply.github.com>

* Fix occured typo in input-dropzone.element.ts

Signed-off-by: SAY-5 <SAY-5@users.noreply.github.com>

* Fix occured typo in CollectibleRuntimeViewCompiler.cs

Signed-off-by: SAY-5 <SAY-5@users.noreply.github.com>

* Fix occured typo in ExamineIndexRebuilder.cs

Signed-off-by: SAY-5 <SAY-5@users.noreply.github.com>

* Fix occured typo in BaseTestDatabase.cs

Signed-off-by: SAY-5 <SAY-5@users.noreply.github.com>

---------

Signed-off-by: SAY-5 <SAY-5@users.noreply.github.com>
Co-authored-by: SAY-5 <SAY-5@users.noreply.github.com>
2026-04-17 06:55:33 +02:00
2955911420 Dependencies: Update minor and patch versions (#22498)
* Update dependencies to latest minors and patches.

* Update test sdk

---------

Co-authored-by: Zeegaan <skrivdetud@gmail.com>
2026-04-17 06:37:52 +02:00
760f6454f4 Backoffice Mocks: Introduce Mock Sets (#22493)
* upgrade msw, migrate all interceptors, and update backoffice integration

* Fix msw test runner integration

* wip mock sets

* align news mock data

* clean up

* add interface for mock sets

* Refactor mock DBs to use dataSet directly

* export as data

* align exports

* Update index.ts

* simplify

* remove createTemplateScaffold from data set

* remove getGroupByName from mock set

* remove getGroupWithResultsByName from mock set

* remove getIndexByName from mock set

* remove unused getSearchResultsMockData function

* Add kenn mock data set with SQLite transformation scripts

Introduces a new "kenn" mock data set generated from an Umbraco SQLite database export.

Transformation scripts (devops/sqlite-to-mock/):
- Database connection helper using sql.js
- Transform scripts for data-types, document-types, media-types, documents, media, users, templates, languages, and dictionary

Generated kenn data set includes:
- 156 data types
- 48 document types with properties, containers, and compositions
- 11 media types
- 41 documents with property values and variants
- 75 media items
- 4 users and 6 user groups
- 10 templates, 2 languages, 5 dictionary items

Usage: VITE_MOCK_SET=kenn npm run dev

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* Fix Tab/Group container type mapping in document types

The PropertyGroupType enum in Umbraco.Core defines:
- Group = 0
- Tab = 1

The transformer had this inverted. Fixed the mapping and regenerated
document-type.data.ts with correct container types.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* Fix user group fallbackPermissions in transformer

Read permissions from umbracoUserGroup2Permission table instead of
the empty userGroupDefaultPermissions column. Also handles mapping
legacy single-letter permission codes to new Umb.Document.* format.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* Remove deprecated createTemplateScaffold from template transformer

This function was removed from the UmbMockDataSet interface.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* Move sqlite-to-mock script to main package.json

Consolidate the SQLite transformation tooling into the main package by:
- Adding sql.js, @types/sql.js, and tsx as devDependencies
- Adding sqlite-to-mock script that runs transformations and lints output
- Removing the separate devops/sqlite-to-mock/package.json and lock file

This allows the transformation scripts to reuse the main node_modules.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* add url manager and url handlers

* Add CLI parameters to sqlite-to-mock script

The script now requires db-path and set-alias arguments:
  npm run sqlite-to-mock -- <db-path> <set-alias>

Changes:
- Add configure(), getDatabase(), getOutputDir(), closeDatabase() for lazy init
- Add CLI argument parsing with validation
- Remove direct execution calls from transform scripts
- Move eslint formatting into the script

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* Generate complete mock data sets and auto-discover sets

- Add generate-supporting-files.ts to create index.ts and all
  placeholder/static files needed for a complete mock data set
- Use import.meta.glob for dynamic mock set discovery instead
  of hardcoded switch statement

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* Add mock handler for document type configuration

Introduces a new mock handler to serve document type configuration data, updates the mock database to provide configuration, and integrates the handler into the document type mock handlers.

* make all mock data optional

* Add custom service worker to bypass static asset requests

Introduces umbServiceWorker.js to intercept and bypass static asset requests before reaching MSW, improving startup performance in Vite development.

* fix type errors

* Update template-query.manager.ts

* change to runtime load of mock data

* Update package-lock.json

* wip mock set switcher

* Use umbMockManager.availableSetNames in mock header

* remove the test set

* clean up

* move mock files to the client project root

* rename folder

* move sqllite tool into mocks folder

* clean up

* rename folder

* update the correct tsconfig file

* Update README.md

* fix path

* mock search

* add mocks for tree siblings endpoint

* add mocks for document type and media type allowed parents

* split user permissions mock data into its own mock set

* Initialize localization registry in date test

* don't use consts. Inits too much from the modules

* Update property-editor-ui-user-picker.test.ts

* Update property-value-cloner-block-grid.cloner.test.ts

* add custom permission

* make test check for custom permission in specific mock set

* use specific mock set with specific user id

* Update document-user-permission.condition.test.ts

* Update section-user-permission.condition.test.ts

* add mock manager util to internal utils

* add import map to test runner

* Move mock-data-set.types and update imports

* Exclude internal consts in export test

* Rename mock key to userPermissions

* Register mock manifests only in development

* manual merge

* Add labels and alias/label list for mock sets

* Add visibility flag for mock sets

* delete kenn mock set

* Update mock-manager.ts

* fix(mocks): correct document type composition generation in sqlite-to-mock

The compositions were reversed - grouped by parentContentTypeId instead
of childContentTypeId. In cmsContentType2ContentType, the parent is the
composed type and the child is the type using it. Also adds inheritance
vs composition detection based on umbracoNode parentId matching.

* Update src/Umbraco.Web.UI.Client/mocks/msw-handlers/member-type/structure.handlers.ts

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update src/Umbraco.Web.UI.Client/mocks/tools/sqlite-to-mock/README.md

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update src/Umbraco.Web.UI.Client/mocks/db/template-detail.manager.ts

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Fix mock DB slice and add safety checks

* Adds "Kitchen Sink" mock data set

* Updates to sqlite-to-mock tool

* Default mock data set tweaks

Replaces "loremflickr.com" images with local placeholders

* "Kitchen Sink" mock data updates

* feat(mocks): add member support to sqlite-to-mock tool

Add transformers for members, member types, and member groups to replace
the empty placeholder arrays. Queries cmsMember, cmsMemberType, and
cmsMember2MemberGroup tables for auth data, property visibility, and
group relationships.

* Updated "Kitchen Sink" mock data with Members

* fix(mocks): type rawData in composition-mapped files to avoid never[] inference

When all compositions arrays are empty, TypeScript infers the element
type as never, causing a type error on the .map() callback. Adding an
explicit type annotation for rawData resolves this. Also fixed in both
generators so future runs produce correctly typed output.

---------

Co-authored-by: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com>
Co-authored-by: leekelleher <leekelleher@gmail.com>
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-04-16 20:31:11 +02:00
Andreas Lykke BorgandGitHub 0f19dc3716 TipTap Code Editor: Fix horizontal overflow in TipTap source code modal (closes #22287) (#22474)
* Fix horizontal scroll when opening Edit source code modal

* Added word-wrap to umb-code-editor
2026-04-16 18:18:09 +00:00
fbe355fdd0 Parameterise variables in SqliteSyntaxProvider and SqlServerSyntaxProvider (#22492)
* fix(SqliteSyntaxProvider.cs): parameterises the `tableName` variable when passing into `DoesPrimaryKeyExist` method

* fix(SqlServerSyntaxProvider.cs): parameterises the `tableName` variable when passing into the `DoesPrimaryKeyExist` sql statement

* test(DoesPrimaryKeyExist-test): Add test file for DoesPrimaryKeyExist

Co-authored-by: LLaverty <liamlaverty@gmail.com>
Co-authored-by: Andy Butland <abutland73@gmail.com>

---------

Co-authored-by: Andy Butland <abutland73@gmail.com>
2026-04-16 11:02:45 +00:00
Andy Butland ad9735f5c5 Merge branch 'release/17.3.4' 2026-04-16 12:41:38 +02:00
Andy ButlandandGitHub d603d0c820 Output Caching: Align Delivery API extensibility with website output caching (#22456)
* Align output cache extension points for the delivery API with those for the website.

* Fix issue running output cache on website and delivery API at the same time.

* Updates from testing.

* Align website default implementation with naming used for delivery API equivalents.

* Addressed code review feedback.

* Updates from self-review.
2026-04-16 09:34:30 +02:00
Andreas ZerbstandGitHub c63e2668d6 Build: Extract nbgv version step into shared template (#22480)
extract nbgv version step into shared template
2026-04-16 07:15:29 +00:00
Andy Butland 87e12b9ee2 Migrations: Fix RetrustForeignKeyAndCheckConstraints failing when data violates a constraint (#22488)
* Fix exception handling in RetrustForeignKeyAndCheckConstraints migration step.

* Addressed code review feedback.
2026-04-16 07:20:38 +02:00
Andy Butland 6ad2a17c09 Bumped version to 17.3.4. 2026-04-16 07:20:09 +02:00
Andy ButlandandGitHub 773ce35e6a Migrations: Fix RetrustForeignKeyAndCheckConstraints failing when data violates a constraint (#22488)
* Fix exception handling in RetrustForeignKeyAndCheckConstraints migration step.

* Addressed code review feedback.
2026-04-16 07:17:22 +02:00
742f0b1e2a Document Editing: Allow removal of template from a document and indicate when the selected template is no longer allowed (closes #20929) (#22348)
* Allow removal of template on a document, and indicate when the selected template is no longer allowed.

* Addressed code review feedback.

* remove duplicate inline color style on template icon

---------

Co-authored-by: engjlr <enl@umbraco.dk>
Co-authored-by: Engiber Lozada <89547469+engijlr@users.noreply.github.com>
2026-04-15 23:39:17 +02:00
CopilotGitHubcopilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>nielslyngsoe
f8da54b79d Add loading indicator to Create menu modals (#20857)
* Initial plan

* Add loading state to document and media create modals

Co-authored-by: nielslyngsoe <6791648+nielslyngsoe@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: nielslyngsoe <6791648+nielslyngsoe@users.noreply.github.com>
2026-04-15 20:31:28 +02:00
HenrikandGitHub 3b11b237cb Code Quality: Eliminate closure in AppPolicedCacheDictionary (#22482)
Eliminate closure
2026-04-15 20:05:58 +02:00
ba5ec202f6 Entity Service: Batch GetAllPaths queries to avoid SQL Server parameter limit (closes #22470) (#22471)
* Group get all paths to avoid exceeding SQL Server's max parameter count.

* Move GetAllPaths batching tests to dedicated test class

Move the explicit SQL Server parameter limit tests into their own
class (EntityServiceGetAllPathsTests) with NewSchemaPerTest so the
raw SqlException surfaces instead of being masked by scope disposal.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 14:55:10 +02:00
37bfe65b77 Umb-icon color setting optimization (#22433)
* use currentColor as color fallback

* clean up necessary prop

* Add test color behavior coverage for umb-icon

---------

Co-authored-by: Engiber Lozada <89547469+engijlr@users.noreply.github.com>
Co-authored-by: engjlr <enl@umbraco.dk>
2026-04-15 12:10:10 +02:00
Jacob OvergaardandClaude Opus 4.6 244ea6c34e CI: Skip Claude review on fork PRs
Fork PRs on the `pull_request` event don't have access to repository
secrets, so the action fails and surfaces a red check on the PR. Guard
the job with a head-repo equality check so the workflow simply doesn't
run for fork PRs. Remove once upstream fork support lands
(anthropics/claude-code-action#939) and `pull_request_target` can be
re-enabled.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-15 11:55:29 +02:00
Jacob OvergaardandClaude Opus 4.6 294b24d20d CI: Revert claude-review trigger to pull_request
pull_request_target fails at OIDC token exchange ("401 Unauthorized -
Invalid OIDC token") against Anthropic's backend, even though the
action itself supports the event (PR #579). Fork PRs will not be
auto-reviewed until the upstream issue is resolved. Kept the
pull_request_target block commented with a pointer to the issues
for when re-enabling becomes viable.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-15 11:44:43 +02:00
Jacob OvergaardandClaude Opus 4.6 ec8f2ccf8e CI: Add actions:read to claude-review workflow permissions
Docs require actions:read at the workflow permissions level in addition
to additional_permissions on the action, so Claude's CI-reading MCP
tools can actually function. See anthropics/claude-code-action
docs/configuration.md.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-15 11:41:51 +02:00
Jacob Welander JensenandGitHub 45da578e97 Media Collection: Display upload notifications in rows rather than in columns (closes #21502) (#22467)
swapping from column to row
2026-04-15 10:53:11 +02:00
015df79ef2 Tags Property Editor: Preserve commas in tag values (closes #22413) (#22432)
* Preserve commas when provided in tags.

* Address code review feedback.

* Split commas for CSV storage, preserve for JSON

* Use tagsInput var and lowercase CSV check

---------

Co-authored-by: Engiber Lozada <89547469+engijlr@users.noreply.github.com>
Co-authored-by: engjlr <enl@umbraco.dk>
2026-04-15 10:19:02 +02:00
3b1956d35b Member Authentication: Add member sign-in/sign-out notifications (closes #22461) (#22463)
* feat(core): add member sign-in/sign-out notifications

Add MemberLoginSuccessNotification, MemberLoginFailedNotification,
and MemberLogoutSuccessNotification to achieve parity with the
existing backoffice user authentication notifications.

Override HandleSignIn in MemberSignInManager to publish login
success/failure notifications, and override SignOutAsync to publish
logout notifications. This follows the same pattern used by
BackOfficeSignInManager for backoffice users.

Closes #22461

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs(core): add remarks to member auth notification classes

Add <remarks> XML documentation to MemberLoginSuccessNotification,
MemberLoginFailedNotification, and MemberLogoutSuccessNotification
describing intended usage, consistent with the backoffice user
notification equivalents.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat(web): use IIpResolver for member auth notification IP addresses

Use IIpResolver.GetCurrentRequestIpAddress() for consistent IP
resolution in member auth notifications, matching the pattern used
by BackOfficeUserManager.

Introduces IIpResolver as a new constructor parameter with the
existing constructor marked obsolete (removal in Umbraco 19) using
StaticServiceProvider fallback for backwards compatibility.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Fixed filing unit tests.

* Added tests for new functionality.

* Ensure MemberFailedNotification is fired on invalid credentials as well as member not found.
Add the reason for the failure to the notification.

* Add tests for other failed notification publishing states.

* Clarified comments.

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Andy Butland <abutland73@gmail.com>
2026-04-15 09:52:18 +02:00
HenrikandGitHub e5bd1954a7 Code Quality: Use more appropriate types for private fields of Umbraco.Cms.Core.Enum<T> (#22475)
Use better types for Umbraco.Cms.Core.Enum<T>, array iterates faster & FrozenDictionary gets better over time. Also fixes naming warnings
2026-04-15 06:54:29 +02:00
HenrikandGitHub a62afe418d Code Quality: Use array over dictionary in private collection of PublishedContentType (#22476)
No need to iterate a Dictionary when an array can be used
2026-04-15 06:49:57 +02:00
Jacob OvergaardandClaude Sonnet 4.6 0eb0313db1 Docs: Remove duplicate TS deprecation section from root CLAUDE.md
The client CLAUDE.md's action-to-doc table already maps deprecation
to docs/deprecation.md, and the root's callout directs agents to read
the client CLAUDE.md for backoffice work. Having the pattern in both
places is redundant.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-14 16:58:44 +02:00
Jacob OvergaardandClaude Sonnet 4.6 5f19354a53 Docs: Add callout to read client CLAUDE.md for backoffice work
Agents working from the repo root now see an explicit instruction to
read the client's CLAUDE.md before touching backoffice code. Prevents
missing project-specific conventions (like UmbDeprecation) that are
documented in the client project but not the root.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-14 16:55:22 +02:00
Jacob OvergaardandClaude Sonnet 4.6 aa2e0a338f Docs: Add action-to-doc checklist to client CLAUDE.md
Maps specific actions (deprecate, create element, add tests, etc.) to
the docs that MUST be read first. Ensures developers opening only the
client folder see the requirements in their Claude context.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-14 16:54:46 +02:00
Jacob OvergaardandClaude Sonnet 4.6 81e6d8ec74 Docs: Add TypeScript deprecation pattern to root CLAUDE.md
The backoffice client requires both @deprecated JSDoc AND a runtime
UmbDeprecation warning for every deprecation. This was documented in
the client's docs/deprecation.md but not referenced in the root
CLAUDE.md, causing AI agents to miss the runtime warning requirement.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-14 16:49:46 +02:00
Jacob OvergaardandClaude Sonnet 4.6 956c4dc830 DevOps: Ignore .claude session artifacts, keep only skills and settings
Broadens the .claude gitignore to ignore everything except skills/
(committed for CI workflows) and settings.json (shared config).
Previously only settings.local.json was ignored, leaving lock files,
worktrees, and scheduled_tasks artifacts untracked but visible.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-14 15:47:31 +02:00
cbe7e8a533 Cache: Invalidate published cache entries when content or media is trashed (#22451)
* Cache: Invalidate published cache entries when content or media is trashed

Trashed content and media were remaining in the published cache because
ContentRefreshNotification/MediaRefreshNotification wrote the trashed
entities back into the cache, and ContentCacheRefresher.HandleMemoryCache
could not resolve the branch descendants after HandleNavigation had moved
them to the recycle bin.

- DocumentCacheService.RefreshContentAsync / MediaCacheService.RefreshMediaAsync:
  early-return for trashed entities, deleting from the database cache and
  removing from the local memory cache.
- DocumentCacheService.RefreshMemoryCacheAsync / MediaCacheService.RefreshMemoryCacheAsync:
  added symmetric else branches so memory cache entries are removed when the
  database cache has no corresponding draft or published node (self-healing).
- ContentCacheRefresher.HandleMemoryCache: added a bin fallback to
  TryGetDescendantsKeys so broadcasted RefreshBranch payloads can resolve
  descendants moved to the recycle bin on load-balanced servers.
- Integration tests covering trashed content and media cache invalidation.

* Cache: Add tests for restoring trashed content and media

Verifies that restored content is back in the draft cache (but not the
published cache, since restore does not republish) and that restored
media is back in the cache.

* Address PR review feedback

- Add bin fallback to MediaCacheRefresher.HandleMemoryCache for
  consistency with ContentCacheRefresher.
- Remove redundant [Test] attributes alongside [TestCase].

* Apply suggestions from code review

Co-authored-by: Andy Butland <abutland73@gmail.com>

---------

Co-authored-by: Andy Butland <abutland73@gmail.com>
2026-04-14 14:32:01 +02:00
9883fb5b42 Backoffice: Add explicit controller aliases to observe() calls in tree components (#22450)
Backoffice: Add explicit controller aliases to observe() calls in tree item and default tree elements

Without explicit aliases, observe() falls back to hashing the callback's
source string on every invocation. The api setter on tree-item-element-base
and the #observeData() method on default-tree.element are called each time
the api property changes, making the hash cost and implicit deduplication
behaviour visible in hot render paths.

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Engiber Lozada <89547469+engijlr@users.noreply.github.com>
2026-04-14 11:20:21 +00:00
Andreas ZerbstandGitHub 3250e69444 E2E: QA: add member type acceptance tests (#22379)
* Updated helpers

* Added tests

* Fixed

* Updated smoke

* Fixes

* Fix smokeTest command in package.json

* Fix typo in smokeTest script key
2026-04-14 08:56:01 +00:00
Niels LyngsøandGitHub 2c7e026721 Store: Accept tokens and update MDs for general Context Consumption (#22458)
* accept a Context Token as well as a string

* update MD files
2026-04-14 08:42:34 +00:00
Callum WhyteandGitHub 8c721dcbde dotnet Templates: Remove legacy Umbraco:CMS:Content:MacroErrors from project template development configuration (#22447)
Remove legacy Umbraco:CMS:Content:MacroErrors from project template Development config
2026-04-14 09:39:26 +02:00
Jacob OvergaardandClaude Sonnet 4.6 fcccd7da54 CI: Use pull_request_target so Claude review runs on fork PRs
pull_request events from forks cannot access OIDC tokens, causing the
job to fail. pull_request_target runs in the base repo context and has
access to secrets/OIDC while still reading the PR diff via the API.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-13 14:27:18 +02:00
7170b45aec Migrations: Quote names when creating index (closes #22409) (#22410)
* fix raw sql

* clean up

* Use existing syntax property.

---------

Co-authored-by: Andy Butland <abutland73@gmail.com>
2026-04-13 11:56:26 +00:00
8f642f24fe Migrations: Consistently handle GUID casing when using SQLite (#22406)
* Ensure MigrationBase formats Guids consistently with NPoco for SQLite

SQLite is case sensitive and doesn't have the concept of uniqueidentifier - Guids are stored as uppercase strings

Add FormatGuid method to SqlSyntaxProvider to centralize the logic

* (Optional) Include default FormatGuid implementation in ISqlSyntaxProvider to make this change non-breaking

* Use ToUpperInvariant for Guids in SQLite

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Tidy up comments, fix existing indentation and add unit tests for GUID formatting.

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Andy Butland <abutland73@gmail.com>
2026-04-13 10:20:38 +00:00
Andy Butland 11fa8a45c8 Merge branch 'release/17.3.3' 2026-04-13 12:12:01 +02:00
Nhu DinhandGitHub bc477c94a9 E2E: QA Updated acceptance tests to match the recent UI changes (#22445)
* Updated tipTapSettings to match the recent changes

* Updated ui helper for insert value/dictionary/partial view button

* Updated api helper for media delivery

* Fixed api helper for verify width and height in vector graphic media
2026-04-13 09:13:50 +00:00
fbc8b605a9 RTE: Block Clipboard label Localization (closes #22412) (#22417)
* localize rte block clipboard entry label

* RTE Block Clipboard: reuse existing localization controller

Avoids alias collision from creating a new UmbLocalizationController on
hosts that already have one. Exposes the base class controller as
protected so subclasses can reuse it.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 10:24:04 +02:00
Jacob OvergaardandClaude Opus 4.6 c56dcfd345 Docs: Clarify PR body must include closing keyword for GitHub auto-close
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 10:00:32 +02:00
Niels LyngsøandGitHub 1ff33e897c General: Add decoding="async" to relevant IMG-tags (#22428)
add decoding="async" to relevant imgs
2026-04-13 09:47:20 +02:00
Jacob Overgaard cbfbeb4272 devops: bump max-turns from 25 to 50 2026-04-13 09:30:15 +02:00
Andy Butland ade77ad00d User Service: Prevent fetching all permissions when no IDs are provided (#22424) 2026-04-11 12:53:34 +02:00
Andy Butland 3275541d92 Bump version to 17.3.3. 2026-04-11 12:50:02 +02:00
Andy ButlandandGitHub 0f4d0a6e67 Basic Authentication: Standalone login page for frontend-only deployments (closes #22144) (#22168)
* Add login for basic authentication without backoffice.

* Add 2FA to basic authentication flow.

* Accessibility improvements.

* Add tests for BasicAuthLoginController.

* Gate controller so only used when basic authentication is enabled.
Use 2FA view even when login page is not configured.

* Add tests for BasicAuthenticationMiddleware.

* Add support for external login providers.

* Addressed code review feedback.

* Applied suggestions from code review.

* Disable and change text on submit button when logging in.

* Add custom view support.
2026-04-11 08:55:08 +00:00
95bcd8fc14 Website Rendering: Add configurable output caching for template rendered pages (#22338)
* Configuration for website output cache settings.

* Interfaces and default implementation for extension points.

* Configure the output cache policy.

* Evict cached documents through updates to related documents, media and members.

* Feedback from code review.

* Update description of service registration in IWebsiteOutputCacheDurationProvider header comment.

Co-authored-by: Sven Geusens <sge@umbraco.dk>

* Use output cache over service provider.

* Optimise and DRY-up eviction handlers.

* Only register IWebsiteOutputCacheManager when the feature is enabled.

* Remove unnecessary check on applying output cache to Umbraco pipeline.

* Add extension point for determining if requests should be cached.

* Broken up large method in DocumentOutputCacheEvictionHandler, put enabled checks around debug logging, further unit test.

---------

Co-authored-by: Sven Geusens <sge@umbraco.dk>
2026-04-11 06:52:03 +00:00
2b4dc2db9a User Service: Prevent fetching all permissions when no IDs are provided (#22424)
* User Service: Prevent fetching all permissions when no IDs are provided

Ensures that the UserService does not attempt to fetch permissions when the provided ID collection is empty, avoiding potentially expensive database queries that could return permissions for all nodes.

* Move guard into the shared private method and add an integration test to verify the fix.

---------

Co-authored-by: Andy Butland <abutland73@gmail.com>
2026-04-10 18:20:38 +00:00
AbdulazizandGitHub 3c50dc7f7b Templates: Fixes modal text styling in when inserting sections (closes #22358) (#22376)
* fixes modal text styling in Insert and Sections in Templates

* fixed review issues and added accesability for the cards so you can use keyboard

* fixing formatting changes

* fixed unused css and fixed accessability to match the card select & deselect

* fixed redundant key and click events

* fixed accessability for button and small bug with not being able to click it
2026-04-10 14:04:41 +00:00
Jacob OvergaardandGitHub 277bd8de62 Clipboard: Localize property labels when copying to clipboard (closes #21998) (#22412) 2026-04-10 13:27:25 +02:00
f9a70b799b Block Grid: Apply language fallback to block elements within layouts (closes #22195) (#22219)
* Apply language fallback to block element expose filtering.

* Handle code review feedback.

* Use builder instead of mocks in tests.

* Fixed failing unit tests.

* Revert previous approach and move fallback handling to the block property value creator.

* Include fallback policy in published property cache key

* Recreate block elements with resolved fallback culture.

* Use correct pattern for dispose.

* Introduce and use PropertyRenderingContext.

* Tidy up Fallback.

* Use core extensions for string comparison

* Less allocations

* Avoid fallback handling when no fallback policies are provided

---------

Co-authored-by: kjac <kja@umbraco.dk>
Co-authored-by: Niels Lyngsø <nsl@umbraco.dk>
2026-04-10 13:10:29 +02:00
Niels LyngsøandGitHub e04cdc2030 Login: Update styles of login screen for better color customizations (#22389)
Update styles of login screen and enables better color customizations
2026-04-10 10:50:50 +00:00
Andy Butland 273f564571 Merge branch 'main' of https://github.com/umbraco/Umbraco-CMS 2026-04-10 12:05:35 +02:00
Andy Butland 08c65ef61f Merge branch 'release/17.3.2' 2026-04-10 12:05:21 +02:00
Jacob OvergaardandClaude Sonnet 4.6 68ec8223bd Docs: Update CLAUDE.md with final Claude workflow architecture [skip ci]
Reflects the two-workflow split, trigger phrase stripping behavior,
allowed tools, labeling for both PRs and issues, and implementation
gotchas discovered during setup.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-10 12:03:02 +02:00
Jacob OvergaardandClaude Sonnet 4.6 6ac48ef10f DevOps: Disable show_full_output now that interactive workflow works
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-10 11:32:49 +02:00
Jacob OvergaardandClaude Sonnet 4.6 2f2722258c DevOps: Remove redundant trigger_phrase (defaults to @claude)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-10 11:28:10 +02:00
Jacob OvergaardandClaude Sonnet 4.6 199beedaac DevOps: Pass PR/issue number explicitly to Claude prompt
Claude couldn't find the PR because checkout is on main and
gh pr view with no args returns nothing. Now the PR number is
injected directly into the prompt from the GitHub event context.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-10 11:27:34 +02:00
Jacob OvergaardandClaude Sonnet 4.6 81c99c0ac8 DevOps: Explicitly prevent umb-review skill and git diff in interactive workflow
Claude discovered and invoked the umb-review skill which uses git diff
against origin/main — but checkout is on main so the diff was empty.
Prompt now explicitly says to use gh pr diff, not git diff or skills.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-10 11:24:34 +02:00
Jacob OvergaardandClaude Sonnet 4.6 f822b89e98 DevOps: Allow npm and dotnet in interactive Claude workflow
Needed for @claude fix scenarios where Claude builds/tests changes.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-10 11:23:17 +02:00
Jacob OvergaardandClaude Sonnet 4.6 48e9e6a814 DevOps: Pre-approve gh and git Bash commands for Claude workflows
The sandbox blocks multi-command Bash operations without approval.
Allow gh and git commands so Claude can read diffs, post comments,
and apply labels without permission errors.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-10 11:22:16 +02:00
Jacob OvergaardandClaude Sonnet 4.6 c900a5346b DevOps: Fix prompt to account for trigger phrase stripping
The action strips @claude from the comment before passing to Claude,
so commands arrive as just 'review', 'fix', etc. Updated prompt to
match. Also default empty messages to review (PR) or help (issue).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-10 11:18:24 +02:00
Jacob OvergaardandClaude Sonnet 4.6 6a0411fa40 DevOps: Restructure interactive prompt around user intent
Prompt now reads the user's message and acts accordingly instead of
prescribing behavior. Common patterns like review/help/fix/label
are listed as examples.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-10 11:15:24 +02:00
Jacob OvergaardandClaude Sonnet 4.6 b7d3236f92 DevOps: Fix interactive workflow prompt to act on PR context
Claude was treating @claude review as a greeting instead of acting
on the PR. Made prompt explicit about reviewing immediately.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-10 11:13:17 +02:00
Jacob OvergaardandClaude Sonnet 4.6 acc99f420f DevOps: Enable show_full_output for debugging interactive workflow
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-10 11:10:51 +02:00
Jacob OvergaardandClaude Sonnet 4.6 d64579f8e3 DevOps: Restore checkout in interactive Claude workflow
Action internally runs git fetch for trusted file restoration.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-10 11:05:00 +02:00
Jacob OvergaardandClaude Sonnet 4.6 f98c2fd3d2 DevOps: Remove checkout from interactive Claude workflow
Action handles repo context via GitHub API — no local files needed.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-10 10:59:06 +02:00
Jacob OvergaardandClaude Sonnet 4.6 3bf5de2559 DevOps: Simplify interactive Claude workflow prompt
Remove umb-review skill reference — auto-review handles thorough reviews.
Interactive workflow gives quick feedback and general assistance.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-10 10:58:39 +02:00
Jacob OvergaardandClaude Sonnet 4.6 f2ff1e850e DevOps: Remove reopened trigger and max-turns limit from auto-review
With only opened/ready_for_review triggers, volume is low enough to
let Claude run without a turn limit.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-10 10:57:39 +02:00
Jacob OvergaardandClaude Sonnet 4.6 17cee849d3 DevOps: Increase auto-review max-turns to 50
25 turns was insufficient — the umb-review skill needs many turns to
read docs, references, changed files, and write the review.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-10 10:56:37 +02:00
Jacob OvergaardandClaude Sonnet 4.6 7fc5a88c95 DevOps: Remove synchronize trigger from auto PR review
Only review on open/reopen/ready — use @claude review for re-reviews.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-10 10:56:16 +02:00
Jacob OvergaardandClaude Sonnet 4.6 f165a9cf3e DevOps: Switch to ANTHROPIC_API_KEY_03
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-10 10:49:08 +02:00
Jacob OvergaardandClaude Sonnet 4.6 3e119cb57f DevOps: Split Claude into two workflows (auto review + interactive)
- claude-review.yml: Auto PR review on open/push/ready (no trigger needed)
- claude.yml: Interactive — @claude comments, issue assignment/labeling

Follows anthropics/claude-code-action official examples pattern.
Full Option B gating on the interactive workflow.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-10 10:46:09 +02:00
Jacob OvergaardandClaude Sonnet 4.6 427a7b264e DevOps: Add issue labeling instructions to Claude workflow
Include affected/*, area/*, and category/* labels for issues.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-10 10:42:17 +02:00
Jacob OvergaardandClaude Sonnet 4.6 52653f780a DevOps: Gate issue_comment events to avoid wasted runners
Only spin up a runner for issue_comment events that mention @claude.
All other event types pass through to the action for internal filtering.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-10 10:41:20 +02:00
Jacob OvergaardandClaude Sonnet 4.6 408a8805c1 DevOps: Set base_branch to main for Claude review workflow
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-10 10:25:02 +02:00
Jacob OvergaardandClaude Sonnet 4.6 1db3f2c8cc DevOps: Set max-turns 25 for Claude review workflow
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-10 10:24:36 +02:00
Jacob OvergaardandClaude Sonnet 4.6 02b75c37e7 DevOps: Add checkout step — action needs git repo on disk
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-10 10:21:19 +02:00
Jacob OvergaardandClaude Sonnet 4.6 27ffb2671c DevOps: Restore prompt with review and issue instructions
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-10 10:19:12 +02:00
Jacob OvergaardandClaude Sonnet 4.6 02d9b50437 DevOps: Simplify Claude workflow to match official documentation
Strip all custom logic — let claude-code-action handle everything.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-10 10:18:40 +02:00
Jacob OvergaardandClaude Sonnet 4.6 98e2eba3fe DevOps: Add actions: read permission to Claude review workflow
Lets Claude see CI status when reviewing PRs.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-10 10:14:19 +02:00
Jacob OvergaardandClaude Sonnet 4.6 a6fe4e6015 DevOps: Consolidate Claude workflows into single file
Merge auto and on-demand review workflows into claude-review.yml.
Add issue support via assignee_trigger and label_trigger.
Let claude-code-action handle permission gating and trigger matching.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-10 10:12:05 +02:00
Jacob OvergaardandClaude Sonnet 4.6 9bfa56afae DevOps: Remove redundant permission check from on-demand review
claude-code-action gates on write permission by default — the manual
getCollaboratorPermissionLevel check was redundant.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-10 10:06:26 +02:00
Jacob OvergaardandClaude Sonnet 4.6 005d65a49f DevOps: Revert trigger phrase to @claude review
Clearer attribution — identifies who is performing the review.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-10 10:04:22 +02:00
Jacob OvergaardandClaude Sonnet 4.6 ed93b184fc DevOps: Add id-token: write permission for claude-code-action OIDC
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-10 10:03:56 +02:00
Jacob OvergaardandClaude Sonnet 4.6 5cc7d53ed2 DevOps: Change on-demand trigger to @umbraco review
Avoids collision with the claude-code-action bot's own @claude trigger.
Re-enables job-level filter to skip non-matching comments early.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-10 10:01:24 +02:00
87be4cfc03 DevOps: Add Claude automated PR review action (closes #AB66809) (#22407)
* DevOps: Add Claude automated PR review action (closes #AB66809)

Adds two GitHub Actions workflows that run the umb-review Claude skill on every non-draft PR and on demand via `@claude review` comments. Reviews are advisory-only and post inline comments per finding plus one summary comment per review run.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* DevOps: Disable auto/on-demand triggers for initial testing

Remove pull_request_target trigger from auto workflow (workflow_dispatch only).
Disable on-demand job until auto workflow is validated.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* enables task

* adds more categories

* DevOps: Address Copilot review feedback

- Checkout PR head ref (not base) so git diff works correctly
- Use fetch-depth: 0 for triple-dot diff merge base
- Fix SHA dedup: use full SHA and paginate comment listing
- Include 'maintain' permission in on-demand gate

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* Docs: Document Claude automated PR review workflows in CLAUDE.md

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-10 09:59:12 +02:00
Andreas Lykke BorgandGitHub 23f4b06cc8 Accessibility: Add label to member type filter dropdown (#22397)
Added missing label to dropdown
2026-04-10 08:00:50 +02:00
Andreas Lykke BorgandGitHub b6b9bc8bf2 Accessibility: Add label and localized placeholder to picker search field (#22402)
Added label and replaced placeholder with localized term
2026-04-10 08:00:27 +02:00
Andreas Lykke BorgandGitHub 06a1e82488 Accessibility: Add labels to member workspace toggles (#22403)
Added labels to toggles missing for accessibility
2026-04-10 08:00:24 +02:00
Laura Neto ea3b0d4d59 Use correct constant for MediaBreadthFirstSeedCount initializer
MediaBreadthFirstSeedCount was initialized with StaticDocumentBreadthFirstSeedCount
instead of StaticMediaBreadthFirstSeedCount, mismatching its [DefaultValue] attribute.
2026-04-09 16:21:50 +02:00
532d10d102 Content picker: Fix display for list items in content picker when pre-selected items exceed maximum (closes #22129) (#22395)
fix issue when display list items in content picker

Co-authored-by: Lan Nguyen Thuy <lnt@umbraco.dk>
2026-04-09 15:35:18 +02:00
4441d6d843 Mock Server: Add missing batch handlers for content types (#22390)
* Fixes MemberType mock handler

* Fixes DocumentType mock handler

* Fixes DataType mock handler

* Fixes MediaType mock handler

* Add readBatch and refactor batch handlers

* Remove 400 response for empty doc-type batch ids

* Use type guard in filter to remove undefined

* remove unused

---------

Co-authored-by: Mads Rasmussen <madsr@hey.com>
2026-04-09 12:28:52 +00:00
Andy Butland 4b1f7e535d Management API: Fix OAuth client registration permanently skipped after transient failure (closes #22356) (#22368)
* Prevent OAuth client registration from being permanently skipped after transient failure.

* Addressed code review feedback.
2026-04-09 14:02:28 +02:00
Andy ButlandandGitHub 8d25312a1a Management API: Fix OAuth client registration permanently skipped after transient failure (closes #22356) (#22368)
* Prevent OAuth client registration from being permanently skipped after transient failure.

* Addressed code review feedback.
2026-04-09 14:00:31 +02:00
Andy ButlandandClaude Opus 4.6 79cf047103 Templating: Move production mode validation from service layer to Management API (#22383)
* Revert production mode validation for templates and partial views at the service layer, and move to management API.

* Remove unused ConfigureProductionMode helper from PartialViewServiceTests

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Add integration tests for UpdateTemplateController production mode behavior

Tests verify that the Management API correctly blocks template content
changes while allowing metadata-only updates in production mode.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Restore partial view service checks.
Add integration tests for template controllers with production mode.

* Align delete with create/update for file system changes in production mode.

* Restore partial view service tests.

* Add test for update to delete template repository.

* Refactored to use single test setup method.

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 12:51:04 +02:00
e5de587721 Templating: Move production mode validation from service layer to Management API (#22383)
* Revert production mode validation for templates and partial views at the service layer, and move to management API.

* Remove unused ConfigureProductionMode helper from PartialViewServiceTests

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Add integration tests for UpdateTemplateController production mode behavior

Tests verify that the Management API correctly blocks template content
changes while allowing metadata-only updates in production mode.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Restore partial view service checks.
Add integration tests for template controllers with production mode.

* Align delete with create/update for file system changes in production mode.

* Restore partial view service tests.

* Add test for update to delete template repository.

* Refactored to use single test setup method.

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 12:44:36 +02:00
Bjarne FyrstenborgandAndy Butland 5d6e3df473 Property Editor Dialog: Set height to 100% for umb-property-editor-ui-picker-modal (#22354)
Set height to 100% for ui-picker-modal element
2026-04-09 11:09:15 +02:00
Andy Butland 0cd35398be Migrations: Fix potential OptimizeInvariantUrlRecords timeout on SQL Server (closes #22377) (#22382)
Ensure parallel execution plans are not used for the OptimizeInvariantUrlRecords migration.
2026-04-09 10:52:56 +02:00
Andy Butland a9615aa729 Bumped version to 17.3.2. 2026-04-09 10:52:15 +02:00
Andy ButlandandGitHub f3863162c8 Migrations: Fix potential OptimizeInvariantUrlRecords timeout on SQL Server (closes #22377) (#22382)
Ensure parallel execution plans are not used for the OptimizeInvariantUrlRecords migration.
2026-04-09 10:48:25 +02:00
Andreas Lykke BorgandGitHub 54b6c22e84 Accessibility: Fix missing labels on uui-select elements causing console warnings (#22385) 2026-04-09 10:08:51 +02:00
Niels Lyngsø 4edfbf44e9 delay condition, good for testing 2026-04-09 09:52:41 +02:00
Niels Lyngsø abc6287008 corect existing usage of map to repeat 2026-04-09 08:05:13 +02:00
20b4529196 Languages: Exclude invariant culture from list of available cultures for language creation (closes #22380) (#22381)
* Exclude invariant culture from culture list endpoint

The Invariant Culture (CultureInfo.InvariantCulture) has an empty Name
property which is not a valid ISO code for Umbraco content. Filter it
out in IsoCodeValidator to prevent it appearing in the culture list.

Fixes #22380

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Add unit tests for IsoCodeValidator.

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Andy Butland <abutland73@gmail.com>
2026-04-08 16:00:05 +00:00
a616e8dc48 Added length validation to change password modal element (#21781)
* feat(Change password modal): Integrate user configuration for minimum password length frontend validation on change-password-modal element.

* feat(change-password-modal): added user-friendly message for password length

* Update minlengthMessage property in change-password modal

* Fix password input minlength attribute syntax

* feat(change-password-modal): enhance password validation with dynamic configuration and feedback

* feat(change-password-modal): prevent form submission while loading configuration based on comment copilot

* Refactor password validation to input validators

* Extract password getter and clarify minimum length guard

* Refactor password validators into helper

---------

Co-authored-by: Niels Lyngsø <nsl@umbraco.dk>
Co-authored-by: Engiber Lozada <89547469+engijlr@users.noreply.github.com>
Co-authored-by: engjlr <enl@umbraco.dk>
2026-04-08 13:35:17 +00:00
Andy ButlandandGitHub 681a510a08 Unit tests: Add test coverage for ContentPermissionService (#22373)
* Add unit tests for ContentPermissionService.

* Addressed code review feedback.

* Used constants for IDs and paths in tests.
2026-04-08 13:32:02 +00:00
88335f871d User group: Fix issue icons not show when different colour than black (closes #22352) (#22372)
Fix issue icons not show when different colour than black

Co-authored-by: Lan Nguyen Thuy <lnt@umbraco.dk>
2026-04-08 15:17:43 +02:00
Andy ButlandandGitHub f0919792a1 Slider: Persist value updates on drag-and-drop (closes #22183) (#22276)
* Persist slider value updates on drag-drop.

* Addressed code review feedback.
2026-04-08 14:35:10 +02:00
3b7d2b9fa9 CSP: Add blob: to img-src for media upload previews (#22343)
Allow "blob:" in local CSP that is necessary for media uploads.

Co-authored-by: Kenn Jacobsen <kja@umbraco.dk>
2026-04-08 11:40:25 +00:00
b3d2cabd59 Claude: Agent MD files for manifestss (#22367)
* manifests.md

* refactor

* clean up unnesecary info

* update to architecture

* update

* Update src/Umbraco.Web.UI.Client/docs/manifests.md

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* update

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-04-08 13:18:08 +02:00
Engiber LozadaandGitHub bb95d74ba8 Content type design: Fix tab overflow with scrollable navigation (closes #20876) (#22294)
* Remove flex-shrink=0 from umb-body-layout

* Avoid collapsing tabs into the dropdown

* Add arrows left and right and bind a scroll

* Add a resizeObserver to keep track when the tabs container change

* Make the sort mode scrollable

* Move the add tab button inside the tabs list container

* Restore tab scrolling and detect hidden overflow

* Create a reusable scrollable container component

* Remove unused import

* Clean up

* Add HTMLElementTagNameMap to the scrollable container

* Always render the add tab button

* Observe slot children on slotchange

* Remove unused variable
2026-04-08 13:03:38 +02:00
1c92cacb83 Cache: Fix published content not immediately routable after PublishBranch (#22341)
* Re-order ContentCacheRefresher handlers so publish status is populated before memory cache refresh.

* Address code review feedback.

* Code tidy.

---------

Co-authored-by: Kenn Jacobsen <kja@umbraco.dk>
2026-04-08 12:42:06 +02:00
4396c3fe4b Integration Tests: Fix raw SQL statements in DocumentUrlTests (closes issue #22360) (#22365)
* fix raw sql statements

* use ISqlSyntaxProvider methods

* Use constants for column names

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Fixed incorrect SQL Count.

* Make SQL more readable.

---------

Co-authored-by: Andy Butland <abutland73@gmail.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-04-08 10:19:23 +00:00
Bjarne FyrstenborgandGitHub ac54cfd467 Property Editor Dialog: Set height to 100% for umb-property-editor-ui-picker-modal (#22354)
Set height to 100% for ui-picker-modal element
2026-04-08 12:04:08 +02:00
2f4d2351d0 Management API: Add document patch endpoint (#22104)
* Document patch, variant name only

* Multi variant tests

* Change to json-patch instead of merge to target nested properties

* Fix ManagementApiTest following PR 20820

* Segment suport for properties

* Verify non existing and trashed document patch behaviour

* Mostly working approuch for nested properties

* Fix endpoint route collision (Somehow...)

* Trying a custom way of doing things

* add escape support, more tests and cleanup

* remove unnecesary using

* Cleanup

* Restore things that are breaking

* cleanup

* Namespace cleanup

* Order cleanup

* More comment updates

* Add default implementations

* Improve modelbinding validation

* all string comparison

* Cleanup unused statuses

* Fix PatchPathResolver Filtering not accepting non string values

* Optimize path parsing

* Improve cookie token rework

* more cleanup

* Put AllowedValues on the correct property 🙈

* One more default implementation

* Add link to docs on endpoint swagger info

* PR review corrections

- Removed leftover affectedCultures & affectedSegments
- Extracted IDocumentPatcher interface
- Optimized serialization in patchEngine by moving it 1 level higher

* Update documentation urls

* Apply suggestions from code review

Co-authored-by: Andy Butland <abutland73@gmail.com>

* Removed affected variance tracking that is nog longer being used

* Extract shared data class

* update claude patching namespace

* Remove no longer valid xml comment

* Fix unittests after refactoring patchengine.ApplyOperation(string,...) to patchengine.ApplyOperation(JsonNode,...)

* Refactor base classes

* Apply suggestions from code review

Co-authored-by: Andy Butland <abutland73@gmail.com>

* Optimizations and refactoring of the patcher/engine/parser

---------

Co-authored-by: Andy Butland <abutland73@gmail.com>
2026-04-08 11:55:22 +02:00
Andy Butland 3541b89380 Merge branch 'release/17.3.1' 2026-04-08 11:48:44 +02:00
Andy Butlandandkjac 6c089db898 Media Picker: Fix folder selection regression for developer-configured media pickers (closes #22349) (#22350)
* Fixes "files/folders/files or folders" selections for the various media picker components, re-allowing folder selection from a media picker.

* Import and use enim instead of hardcoded enum value

---------

Co-authored-by: kjac <kja@umbraco.dk>
2026-04-07 16:26:37 +02:00
Andy Butland 70dd464346 Builder Extensions: Make AddWebComponents() idempotent (closes #22344) (#22347)
Ensure AddWebComponents is idempotent.
2026-04-07 16:26:27 +02:00
Andy Butland 8c5c6a8870 Install: Ensure media directory exists before creating PhysicalFileProvider (closes #14877) (#22281)
* Ensure media directory exists before creating PhysicalFileProvider.

* Ensure file provider is disposed in test.
2026-04-06 13:02:45 +02:00
Andy Butland 2b3f468111 Document URL Service: Batch delete of obsolete URL segment records to avoid SQL Server parameter limit (closes #22339) (#22340)
* Batch delete in DocumentUrlRepository and DocumentUrlAliasRepository to avoid exceeding SQL Server's 2100 parameter limit.

* Address code review feedback.

* Remove the unnecessary trigger rebuild on startup statement in the SQL Server migration path.
2026-04-03 12:38:49 +02:00
Andy Butland 727dd02a9e Bumped version to 17.3.1. 2026-04-03 11:35:54 +02:00
2100 changed files with 107968 additions and 18633 deletions
+94
View File
@@ -0,0 +1,94 @@
---
name: umb-bump-version
description: Bump the Umbraco CMS version across all required files. Use when the user asks to bump, update, or set the version number — e.g., "bump version to 17.3.4", "set version to 18.0.0-rc", "update version". Accepts the target version as an argument.
argument-hint: <version> (e.g., 17.3.4, 18.0.0-rc)
---
# Bump Version - Umbraco CMS
Updates the Umbraco CMS version string across all files that track it.
**Do NOT use AskUserQuestion if a version argument is provided. Only ask if `$ARGUMENTS` is empty or cannot be parsed as a version.**
## Arguments
- `$ARGUMENTS` - Required: the target version string (e.g., `17.3.4`, `18.0.0-rc`)
## Files to Update
The following 5 files must be updated with the new version:
| # | File | Field |
|---|------|-------|
| 1 | `version.json` | `"version"` |
| 2 | `src/Umbraco.Web.UI.Client/package.json` | `"version"` |
| 3 | `src/Umbraco.Web.UI.Client/package-lock.json` | top-level `"version"` AND `packages[""].version` |
| 4 | `tests/Umbraco.Tests.AcceptanceTest/package.json` | `"version"` |
| 5 | `tests/Umbraco.Tests.AcceptanceTest/package-lock.json` | top-level `"version"` AND `packages[""].version` |
**Note**: For major version bumps (e.g., 17.x to 18.x), `src/Umbraco.Web.UI.Login/package.json` has a caret-ranged dependency on `@umbraco-cms/backoffice` (e.g., `^17.2.0`) that will need manual updating. This skill does not handle that — major bumps involve many other changes beyond version strings.
## Instructions
### 1. Parse and Validate the Version
Extract the version from `$ARGUMENTS`. It must be a valid semver-like string (e.g., `17.3.4`, `18.0.0-rc`, `17.4.0-preview.1`). If no version is provided or it cannot be parsed, ask the user for the target version.
### 2. Read the Current Version
Read `version.json` and extract the current `"version"` value. If the current version already equals the target version, report that the version is already set and stop — do not edit, stage, or commit anything.
Otherwise, display both versions:
```
Bumping version: {current} -> {target}
```
### 3. Update All Files
Update each of the 5 files listed above, replacing the old version with the new version. For each file:
- **`version.json`**: Replace the `"version"` value.
- **`package.json` files**: Replace the `"version"` value (near the top of the file).
- **`package-lock.json` files**: Replace BOTH the top-level `"version"` value AND the `"version"` inside the `"packages": { "": { ... } }` block. These are always in the first ~10 lines of the file.
Use targeted edits — do NOT rewrite entire files. Be precise to avoid changing version strings in dependency entries.
### 4. Verify
After all edits, grep for the target version value across the 5 files to confirm all updates landed correctly:
```bash
grep -n "\"version\": \"{version}\"" version.json src/Umbraco.Web.UI.Client/package.json src/Umbraco.Web.UI.Client/package-lock.json tests/Umbraco.Tests.AcceptanceTest/package.json tests/Umbraco.Tests.AcceptanceTest/package-lock.json
```
Expect exactly 7 matches (one per `package.json` and `version.json`, two per `package-lock.json`).
### 5. Stage and Commit
Stage only the 5 changed files:
```bash
git add version.json src/Umbraco.Web.UI.Client/package.json src/Umbraco.Web.UI.Client/package-lock.json tests/Umbraco.Tests.AcceptanceTest/package.json tests/Umbraco.Tests.AcceptanceTest/package-lock.json
```
Then commit with the message `Bump version to {version}.` — replacing `{version}` with the target version:
```bash
git commit -m "Bump version to {version}."
```
### 6. Report
Output a summary:
```
Version bumped to {version} in:
- version.json
- src/Umbraco.Web.UI.Client/package.json
- src/Umbraco.Web.UI.Client/package-lock.json
- tests/Umbraco.Tests.AcceptanceTest/package.json
- tests/Umbraco.Tests.AcceptanceTest/package-lock.json
Changes staged and committed.
```
+1 -1
View File
@@ -23,7 +23,7 @@ jobs:
runs-on: ubuntu-latest
name: Build and Deploy Job
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
with:
submodules: true
- name: Build And Deploy
+1 -1
View File
@@ -26,7 +26,7 @@ jobs:
runs-on: ubuntu-latest
name: Build and Deploy Job
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- name: Build And Deploy
id: builddeploy
uses: Azure/static-web-apps-deploy@v1
+88
View File
@@ -0,0 +1,88 @@
name: Claude PR Review
on:
pull_request:
types: [opened, ready_for_review, reopened]
# NOTE: `pull_request_target` would let this workflow review fork PRs
# (with access to secrets), but the action currently fails during OIDC
# token exchange with "401 Unauthorized - Invalid OIDC token" on that
# event. PR #579 added `pull_request_target` routing to the action, but
# Anthropic's `/github-app-token-exchange` endpoint appears not to
# accept the token claims produced by that event. Re-enable once the
# upstream issue is resolved.
# See: https://github.com/anthropics/claude-code-action/issues/347
# https://github.com/anthropics/claude-code-action/issues/621
# pull_request_target:
# types: [opened, ready_for_review]
permissions:
contents: read
pull-requests: write
id-token: write
actions: read
jobs:
review:
# Skip fork PRs: secrets are not exposed on `pull_request` events from
# forks, so the action would fail with a red check. Remove this clause
# once upstream fork support lands (tracked in
# https://github.com/anthropics/claude-code-action/issues/939) and we
# can re-enable the `pull_request_target` trigger above.
if: >-
github.event.pull_request.draft == false
&& github.event.pull_request.head.repo.full_name == github.repository
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
with:
fetch-depth: 1
- uses: anthropics/claude-code-action@v1
with:
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY_03 }}
# Enable progress tracking
track_progress: true
# Debug (set to true to show full output in logs, false to hide it and only post comments on the PR)
show_full_output: false
additional_permissions: "actions: read"
claude_args: "--model claude-sonnet-4-6 --allowedTools 'Bash(gh:*),Bash(git:*)'"
prompt: |
You are reviewing pull request #${{ github.event.pull_request.number }}
in the Umbraco CMS repository.
Read and execute the review procedure defined in `.claude/skills/umb-review/SKILL.md`.
For each finding that references a specific file and line:
- Post an individual inline PR comment on that line.
- Format: **[Severity]** explanation, then suggestion.
For the overall summary (header, impact, verdict):
- Post ONE top-level PR comment.
Do NOT use sticky/updating comments — post new individual comments.
After reviewing, apply labels to the PR based on changed files:
- `area/frontend` — if files under `src/Umbraco.Web.UI.Client/` are changed
- `area/backend` — if .cs files outside the frontend client are changed
- `area/test` — if only test files are changed
- `category/api` — if Management API or Delivery API files are changed
- `category/breaking` — if breaking changes were detected in the review
- `category/localization` — if localization/language files are changed
- `category/test-automation` — if only test files are changed
- `category/refactor` — if the PR is pure refactoring with no new features
- `category/performance` — if performance-related changes are detected
- `category/ux` — if user-facing changes are detected
- `category/ui` — if changes to the UI layer are detected
Only apply labels you are confident about. Never remove existing labels.
Be friendly and constructive. This project values community contributions.
Frame feedback as suggestions where possible.
Reserve firm language for genuine Critical issues only.
Run fully autonomously. Do NOT ask questions.
Only review changed files. Do not flag pre-existing issues.
Do not suggest changes that would themselves introduce breaking changes.
+91
View File
@@ -0,0 +1,91 @@
name: Claude
on:
issue_comment:
types: [created]
pull_request_review_comment:
types: [created]
issues:
types: [opened, assigned, labeled]
pull_request_review:
types: [submitted]
permissions:
contents: read
pull-requests: write
issues: write
id-token: write
jobs:
claude:
if: |
(github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) ||
(github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude')) ||
(github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude')) ||
(github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude')))
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
with:
fetch-depth: 1
- uses: anthropics/claude-code-action@v1
with:
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY_03 }}
assignee_trigger: "claude"
label_trigger: "claude"
base_branch: "main"
additional_permissions: "actions: read"
claude_args: "--model claude-sonnet-4-6 --max-turns 50 --allowedTools 'Bash(gh:*),Bash(git:*),Bash(npm:*),Bash(dotnet:*)'"
prompt: |
You are an AI assistant for the Umbraco CMS repository, an open-source
.NET CMS that welcomes community contributions.
You were triggered on issue/PR #${{ github.event.issue.number || github.event.pull_request.number }}.
Read the user's message and do what they ask. The trigger phrase
`@claude` is stripped before you see the message, so common requests
will look like:
- `review` — Review PR #${{ github.event.issue.number || github.event.pull_request.number }}.
Use `gh pr diff ${{ github.event.issue.number || github.event.pull_request.number }}`
and `gh pr view ${{ github.event.issue.number || github.event.pull_request.number }}`
to read the changes. Do NOT use git diff or the umb-review skill.
Focus on bugs, breaking changes, and architectural concerns.
Post inline comments for specific issues and a brief summary.
- `help` or a general question — Answer based on the codebase.
Read CLAUDE.md files for project structure and conventions.
- `fix ...` — Implement the requested fix on a new branch.
- `label` — Apply appropriate labels to the PR or issue.
If the message is empty or just whitespace, treat it as `review`
when on a PR, or `help` when on an issue.
If none of these match, read the user's message carefully and respond
to what they actually asked for.
## Labeling
When labeling PRs (based on changed files):
- `area/frontend`, `area/backend`, `area/test`
- `category/api`, `category/breaking`, `category/localization`
- `category/refactor`, `category/performance`, `category/ux`, `category/ui`
- `category/test-automation`
When labeling issues (based on content):
- `area/frontend`, `area/backend`, `area/test`
- `affected/v14` through `affected/v17`, `affected/backoffice`
- `category/api`, `category/localization`, `category/performance`
- `category/ux`, `category/ui`
Only apply labels you are confident about. Never remove existing labels.
## Tone
Be friendly and constructive. Frame feedback as suggestions.
Reserve firm language for genuine critical issues only.
## Constraints
- Run fully autonomously. Do NOT ask questions.
- Do not suggest changes that would introduce breaking changes.
+1 -1
View File
@@ -51,7 +51,7 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v6
# We use the setup-dotnet action to set up .NET Core, otherwise the CodeQL CLI will not work with preview versions.
- name: Setup .NET from global.json
+84
View File
@@ -0,0 +1,84 @@
name: Issue Deduplication
on:
issues:
types: [ opened ]
workflow_dispatch:
inputs:
issue_number:
description: 'Issue number to analyze for duplicates'
required: true
type: number
jobs:
deduplicate:
runs-on: ubuntu-latest
timeout-minutes: 10
permissions:
contents: read
issues: write
id-token: write
steps:
- name: Checkout repository
uses: actions/checkout@v6
with:
fetch-depth: 1
- name: Check for duplicate issues
uses: anthropics/claude-code-action@v1
with:
prompt: |
Analyze this new issue and check if it's a duplicate of existing issues in the repository.
Issue: #${{ github.event.issue.number || inputs.issue_number }}
Repository: ${{ github.repository }}
Your task:
1. Use mcp__github__get_issue to get details of the current issue (#${{ github.event.issue.number || inputs.issue_number }})
2. Search for similar existing issues using mcp__github__search_issues with relevant keywords from the issue title and body
3. Compare the new issue with existing ones to identify potential duplicates
Criteria for duplicates:
- Same bug or error being reported
- Same feature request (even if worded differently)
- Same question being asked
- Issues describing the same root problem
If you find duplicates:
- Add a comment on the new issue linking to the original issue(s)
- Apply the "duplicate" and "state/needs-investigation" labels to the new issue
- Be polite and explain why it's a duplicate
- Suggest the user follow the original issue for updates
If it's NOT a duplicate:
- Don't add any comments
- You may apply appropriate topic labels based on the issue content
Use these tools:
- mcp__github__get_issue: Get issue details
- mcp__github__search_issues: Search for similar issues
- mcp__github__list_issues: List recent issues if needed
- mcp__github__add_issue_comment: Add a comment if duplicate found
- mcp__github__update_issue: Add labels
Be thorough but efficient. Focus on finding true duplicates, not just similar issues.
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY_03 }}
# Issues are opened by community members without write access, so the
# default OIDC token exchange fails with "User does not have write
# access on this repository". Pass `github_token` explicitly and set
# `allowed_non_write_users` to bypass that check. Safe here because
# `permissions:` and `--allowedTools` below are tightly scoped to
# issue operations only.
github_token: ${{ secrets.GITHUB_TOKEN }}
allowed_non_write_users: "*"
# Surface full SDK output (including tool calls and permission denials)
# to diagnose why Claude sometimes only partially completes (e.g. labels
# an issue but skips the comment). Safe to leave on — no secrets in output.
show_full_output: true
claude_args: |
--model claude-haiku-4-5 --allowedTools "mcp__github__get_issue,mcp__github__search_issues,mcp__github__list_issues,mcp__github__add_issue_comment,mcp__github__update_issue,mcp__github__get_issue_comments"
+2 -2
View File
@@ -34,7 +34,7 @@ jobs:
run:
working-directory: src/Umbraco.Web.UI.Client
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- name: Use Node.js
uses: actions/setup-node@v4
with:
@@ -57,7 +57,7 @@ jobs:
run:
working-directory: src/Umbraco.Web.UI.Client
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- name: Use Node.js
uses: actions/setup-node@v4
with:
+5 -2
View File
@@ -52,7 +52,9 @@ tools/docfx/
/build/csharp-docs/_site/
# Local config
.claude/settings.local.json
.claude/*
!.claude/skills/
!.claude/settings.json
.env.local
# Build
@@ -70,7 +72,8 @@ tools/docfx/
/src/Umbraco.Cms.StaticAssets/wwwroot/umbraco/assets
/src/Umbraco.Cms.StaticAssets/wwwroot/umbraco/js
/src/Umbraco.Cms.StaticAssets/wwwroot/umbraco/lib
/src/Umbraco.Cms.StaticAssets/wwwroot/umbraco/views
/src/Umbraco.Cms.StaticAssets/wwwroot/umbraco/views/*
!/src/Umbraco.Cms.StaticAssets/wwwroot/umbraco/views/errors
/src/Umbraco.Cms.StaticAssets/wwwroot/umbraco/login
# Environment specific data
+94 -2
View File
@@ -198,7 +198,7 @@ Use the format: `Area: Description (closes #IssueID)`
- Describe the change and its impact
- Be specific, not vague (describe "a golden retriever" not just "a dog")
**Issue Linking**: Add `(closes #IssueID)` to auto-close linked issues on merge.
**Issue Linking**: Add `(closes #IssueID)` to the title for readability, AND include a closing keyword on its own line in the PR body (e.g., `Fixes #IssueID`) so GitHub actually auto-links and auto-closes the issue on merge. GitHub only parses closing keywords (`closes`, `fixes`, `resolves`) from the PR body or commit messages — the title suffix is cosmetic and does **not** trigger auto-close on its own.
### Commit Messages
@@ -227,9 +227,11 @@ Project ownership is distributed across teams. Check individual project director
1. **Layered Architecture with Dependency Inversion**
- Core defines contracts (interfaces)
- Infrastructure implements contracts
- Infrastructure implements contracts that need Infrastructure-owned machinery
- Web/APIs consume implementations via DI
**Where service implementations live**: Services whose dependencies are satisfiable from Core interfaces alone (repositories, scope, config, other Core services) live in `Umbraco.Core/Services/` — this covers the majority of domain services (`MemberService`, `ContentService`, `MediaService`, `ContentTypeService`, `EntityService`, `AuditService`, `ExternalMemberService`, etc.). Service implementations only live in `Umbraco.Infrastructure/Services/Implement/` when they genuinely need Infrastructure concerns — Examine indexes (`ContentSearchService`, `MediaSearchService`, `IndexedEntitySearchService`), log files (`LogViewerRepository`), packaging internals (`PackagingService`), webhook firing (`WebhookFiringService`), distributed-job coordination (`DistributedJobService`). When adding a new service, default to Core and only move to Infrastructure if a concrete dependency forces it.
2. **Interface-First Design**
- All services defined as interfaces in Core
- Enables testing, polymorphism, extensibility
@@ -441,6 +443,94 @@ The backoffice is published to npm as `@umbraco-cms/backoffice`. Runtime depende
---
## 7. CI/CD — Claude AI Assistant
Two GitHub Actions workflows powered by `anthropics/claude-code-action@v1`. Advisory only — does not block merging.
### Workflows
| File | Trigger | Purpose |
|------|---------|---------|
| `claude-review.yml` | `pull_request: [opened, ready_for_review]` | Auto-review every non-draft PR using the `umb-review` skill |
| `claude.yml` | `@claude` comments, issue assign/label | Interactive assistant for PRs and issues |
### Auto-Review (`claude-review.yml`)
Runs the full `.claude/skills/umb-review/SKILL.md` procedure on every newly opened or un-drafted PR. Produces inline comments per finding and one summary comment with a verdict. Skips draft PRs. No turn limit.
### Interactive (`claude.yml`)
Responds to `@claude` mentions on PRs and issues. The trigger phrase is stripped before Claude sees the message, so:
- `@claude review` → light review using `gh pr diff` (not the umb-review skill)
- `@claude fix ...` → implements a fix on a new branch
- `@claude help` → answers questions about the codebase
- `@claude label` → applies labels
- `@claude` (empty) → defaults to `review` on PRs, `help` on issues
Also triggers on issue assignment to `claude` or adding the `claude` label. Gated: only runs when `@claude` appears in the comment/issue body. Max 25 turns.
**Allowed Bash tools**: `gh`, `git`, `npm`, `dotnet` (interactive only; auto-review allows `gh` and `git`).
### Labels
Both workflows apply labels based on content:
**On PRs** (based on changed files):
| Label | Condition |
|-------|-----------|
| `area/frontend` | Files under `src/Umbraco.Web.UI.Client/` |
| `area/backend` | `.cs` files outside the frontend client |
| `area/test` | Only test files changed |
| `category/api` | Management or Delivery API files |
| `category/breaking` | Breaking changes detected |
| `category/localization` | Localization/language files |
| `category/test-automation` | Only test files changed |
| `category/refactor` | Pure refactoring, no new features |
| `category/performance` | Performance-related changes |
| `category/ux` | User-facing changes |
| `category/ui` | UI layer changes |
**On Issues** (based on content): same `area/*` and `category/*` labels, plus `affected/v14` through `affected/v17` and `affected/backoffice`.
Labels are only added, never removed. Claude applies only labels it is confident about.
### Key Implementation Notes
- **Checkout required** — the action internally runs `git fetch origin main` for trusted file restoration. Without `actions/checkout`, it fails with `fatal: not a git repository`.
- **`id-token: write` permission** — required for OIDC token exchange with the Claude GitHub App.
- **Trigger phrase stripping** — the action strips `@claude` from comments before passing to Claude. Prompts must reference commands without the prefix (e.g., `review` not `@claude review`).
- **PR number injection** — the interactive workflow injects the PR/issue number into the prompt via `${{ github.event.issue.number }}` since Claude can't discover it from `gh pr view` when checked out on `main`.
---
## 8. Code Comment Policy
**Default to no comment.** Applies to all code in this repository — C#, TypeScript, Razor, build scripts. Well-named identifiers and small functions are the primary form of self-documentation; comments are a fallback for the rare cases where the code itself cannot carry the meaning.
### When NOT to comment
- **Don't restate what the code does.** A line calling `resetState()` does not need `// Reset state`. A method named `validateInput` does not need `// Validate input`.
- **Don't narrate a sequence of calls.** If three lines run in order, the order is in the code — don't paraphrase it above.
- **Don't reference the current task, fix, callers, or PR.** No `// Fix for X`, `// Used by Y`, `// Added for the Z flow`, `// See PR #1234`. That belongs in commit messages and PR descriptions; in source it rots as the codebase evolves.
### When a comment IS justified
Write a comment only when **removing it would leave a future reader confused**. Concretely:
- **A non-obvious WHY.** A hidden constraint, business rule, or ordering requirement that is not visible from the code.
- **A workaround for a specific bug or platform quirk.** Link the issue (`(#21996)`, `https://...`) so the comment can be deleted once the upstream fix lands.
- **A subtle invariant** that the type system or method names do not enforce.
- **An edge case the code intentionally handles** that would surprise a reader (e.g. "must run before X because Y").
- **API documentation** — XML doc comments on C# members, JSDoc on exported TypeScript symbols. Required for the public contract; still keep them concise.
### TODOs
Allowed, but cheap to write and cheaper to leave behind. Keep them short and trackable: `// TODO (V19): remove once obsolete overload is gone` or `// TODO: pagination [NL]`. A TODO should have an author or a version trigger.
---
## Quick Reference
### Essential Commands
@@ -499,6 +589,8 @@ For detailed information about individual projects, see their CLAUDE.md files:
- **API Infrastructure**: `/src/Umbraco.Cms.Api.Common/CLAUDE.md` - OpenAPI, authentication, serialization
- **Backoffice Frontend**: `/src/Umbraco.Web.UI.Client/CLAUDE.md` - Lit web components, extension system, auth client
**Important**: When working on backoffice client code (anything under `src/Umbraco.Web.UI.Client/`), read `/src/Umbraco.Web.UI.Client/CLAUDE.md` first. It contains action-specific checklists (deprecation, testing, security, etc.) that are not duplicated here.
### Getting Help
- **Official Docs**: https://docs.umbraco.com/
+29 -26
View File
@@ -13,32 +13,32 @@
</ItemGroup>
<!-- Microsoft packages -->
<ItemGroup>
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation" Version="10.0.4" />
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation" Version="10.0.6" />
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="4.14.0" />
<PackageVersion Include="Microsoft.Data.Sqlite" Version="10.0.4" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="10.0.4" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.SqlServer" Version="10.0.4" />
<PackageVersion Include="Microsoft.Extensions.Caching.Abstractions" Version="10.0.4" />
<PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="10.0.4" />
<PackageVersion Include="Microsoft.Extensions.Configuration.Abstractions" Version="10.0.4" />
<PackageVersion Include="Microsoft.Extensions.Configuration.Json" Version="10.0.4" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="10.0.4" />
<PackageVersion Include="Microsoft.Extensions.FileProviders.Embedded" Version="10.0.4" />
<PackageVersion Include="Microsoft.Extensions.FileProviders.Physical" Version="10.0.4" />
<PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="10.0.4" />
<PackageVersion Include="Microsoft.Extensions.Http" Version="10.0.4" />
<PackageVersion Include="Microsoft.Extensions.Identity.Core" Version="10.0.4" />
<PackageVersion Include="Microsoft.Extensions.Identity.Stores" Version="10.0.4" />
<PackageVersion Include="Microsoft.Extensions.Logging" Version="10.0.4" />
<PackageVersion Include="Microsoft.Extensions.Options" Version="10.0.4" />
<PackageVersion Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="10.0.4" />
<PackageVersion Include="Microsoft.Extensions.Options.DataAnnotations" Version="10.0.4" />
<PackageVersion Include="Microsoft.Extensions.Caching.Hybrid" Version="10.4.0" />
<PackageVersion Include="Microsoft.Data.Sqlite" Version="10.0.6" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="10.0.6" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.SqlServer" Version="10.0.6" />
<PackageVersion Include="Microsoft.Extensions.Caching.Abstractions" Version="10.0.6" />
<PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="10.0.6" />
<PackageVersion Include="Microsoft.Extensions.Configuration.Abstractions" Version="10.0.6" />
<PackageVersion Include="Microsoft.Extensions.Configuration.Json" Version="10.0.6" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="10.0.6" />
<PackageVersion Include="Microsoft.Extensions.FileProviders.Embedded" Version="10.0.6" />
<PackageVersion Include="Microsoft.Extensions.FileProviders.Physical" Version="10.0.6" />
<PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="10.0.6" />
<PackageVersion Include="Microsoft.Extensions.Http" Version="10.0.6" />
<PackageVersion Include="Microsoft.Extensions.Identity.Core" Version="10.0.6" />
<PackageVersion Include="Microsoft.Extensions.Identity.Stores" Version="10.0.6" />
<PackageVersion Include="Microsoft.Extensions.Logging" Version="10.0.6" />
<PackageVersion Include="Microsoft.Extensions.Options" Version="10.0.6" />
<PackageVersion Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="10.0.6" />
<PackageVersion Include="Microsoft.Extensions.Options.DataAnnotations" Version="10.0.6" />
<PackageVersion Include="Microsoft.Extensions.Caching.Hybrid" Version="10.5.0" />
<PackageVersion Include="System.Linq.Async" Version="7.0.0" />
</ItemGroup>
<!-- Umbraco packages -->
<ItemGroup>
<PackageVersion Include="Umbraco.JsonSchema.Extensions" Version="0.3.0" />
<PackageVersion Include="Umbraco.JsonSchema.Extensions" Version="0.4.0" />
</ItemGroup>
<!-- Third-party packages -->
<ItemGroup>
@@ -50,7 +50,7 @@
<PackageVersion Include="HtmlAgilityPack" Version="1.12.4" />
<PackageVersion Include="JsonPatch.Net" Version="3.3.0" />
<PackageVersion Include="K4os.Compression.LZ4" Version="1.3.8" />
<PackageVersion Include="MailKit" Version="4.15.1" />
<PackageVersion Include="MailKit" Version="4.16.0" />
<PackageVersion Include="Markdig" Version="0.45.0" />
<PackageVersion Include="Markdown" Version="2.2.1" />
<PackageVersion Include="MessagePack" Version="3.1.4" />
@@ -59,9 +59,9 @@
<PackageVersion Include="ncrontab" Version="3.4.0" />
<PackageVersion Include="NPoco" Version="6.2.0" />
<PackageVersion Include="NPoco.SqlServer" Version="6.2.0" />
<PackageVersion Include="OpenIddict.Abstractions" Version="7.2.0" />
<PackageVersion Include="OpenIddict.AspNetCore" Version="7.2.0" />
<PackageVersion Include="OpenIddict.EntityFrameworkCore" Version="7.2.0" />
<PackageVersion Include="OpenIddict.Abstractions" Version="7.4.0" />
<PackageVersion Include="OpenIddict.AspNetCore" Version="7.4.0" />
<PackageVersion Include="OpenIddict.EntityFrameworkCore" Version="7.4.0" />
<PackageVersion Include="Serilog" Version="4.3.1" />
<PackageVersion Include="Serilog.AspNetCore" Version="9.0.0" />
<PackageVersion Include="Serilog.Enrichers.Process" Version="3.0.0" />
@@ -77,7 +77,7 @@
<PackageVersion Include="SixLabors.ImageSharp" Version="3.1.12" />
<PackageVersion Include="SixLabors.ImageSharp.Web" Version="3.2.0" />
<!-- When updating this version, also update templates/UmbracoExtension/Umbraco.Extension.csproj -->
<PackageVersion Include="Swashbuckle.AspNetCore" Version="10.1.4" />
<PackageVersion Include="Swashbuckle.AspNetCore" Version="10.1.7" />
</ItemGroup>
<!-- Transitive pinned versions (only required because our direct dependencies have vulnerable versions of transitive dependencies) -->
<ItemGroup>
@@ -88,5 +88,8 @@
<!-- Markdown references vulnerable version of the following: -->
<!-- TODO (V19): Remove these pinned dependencies when the Markdown dependency is removed. -->
<PackageVersion Include="System.Text.RegularExpressions" Version="4.3.1" />
<!-- Examine (via Microsoft.AspNetCore.DataProtection 8.0.4) references a vulnerable version of the following: -->
<!-- TODO: Remove this pinned dependency when Examine updates its Microsoft.AspNetCore.DataProtection reference. -->
<PackageVersion Include="System.Security.Cryptography.Xml" Version="10.0.6" />
</ItemGroup>
</Project>
+29 -15
View File
@@ -188,16 +188,9 @@ stages:
parameters:
nodeVersion: ${{ variables.nodeVersion }}
npm_config_cache: ${{ variables.npm_config_cache }}
- bash: |
echo "##[command]Install nbgv"
dotnet tool install --tool-path . nbgv
echo "##[command]Running nbgv get-version"
PACKAGE_VERSION=$(nbgv get-version -v NpmPackageVersion)
echo "##[command]Running npm version"
echo "##[debug]Version: $PACKAGE_VERSION"
cd tests/Umbraco.Tests.AcceptanceTest
npm version $PACKAGE_VERSION --allow-same-version --no-git-tag-version
displayName: Set NPM Version
- template: templates/set-npm-version.yml
parameters:
workingDirectory: tests/Umbraco.Tests.AcceptanceTest
- bash: |
echo "##[command]Running npm pack"
mkdir $(Build.ArtifactStagingDirectory)/npm-testhelpers
@@ -904,12 +897,27 @@ stages:
- stage: Deploy_NuGet
displayName: NuGet release
dependsOn: Deploy_MyGet
condition: and(succeeded(), or(eq(dependencies.Build.outputs['A.build.NBGV_PublicRelease'], 'True'), ${{parameters.nuGetDeploy}}))
# Run only when Deploy_MyGet actually ran (succeeded or failed) — not when it was skipped due to an upstream test failure.
# Inspect Deploy_MyGet's direct result rather than succeeded()/failed(), which are transitive across the full ancestor graph.
# Approval is required every run via the WaitForApproval job below.
condition: and(in(dependencies.Deploy_MyGet.result, 'Succeeded', 'SucceededWithIssues', 'Failed'), or(eq(dependencies.Build.outputs['A.build.NBGV_PublicRelease'], 'True'), ${{parameters.nuGetDeploy}}))
jobs:
- job:
- job: WaitForApproval
displayName: Wait for manual approval
pool: server
timeoutInMinutes: 4320 # 3 days
steps:
- task: ManualValidation@0
displayName: Manual approval to push to NuGet
inputs:
notifyUsers: ''
instructions: 'Approve to push the NuGet release.'
onTimeout: 'reject'
- job: Push
displayName: Push to NuGet
dependsOn: WaitForApproval
pool:
vmImage: "windows-latest" # NuGetCommand@2 is no longer supported on Ubuntu 24.04 so we'll use windows until an alternative is available.
displayName: Push to NuGet
steps:
- checkout: none
- task: DownloadPipelineArtifact@2
@@ -927,7 +935,10 @@ stages:
- stage: Deploy_Npm
displayName: Npm release
condition: and(succeeded(), or(eq(dependencies.Build.outputs['A.build.NBGV_PublicRelease'], 'True'), ${{parameters.npmDeploy}}))
# Inspect Deploy_NuGet.result directly so a MyGet failure (which is in the transitive ancestor graph)
# doesn't cascade-skip this stage via succeeded(). Deploy_NuGet must itself have succeeded — a NuGet
# failure deliberately blocks the npm release.
condition: and(in(dependencies.Deploy_NuGet.result, 'Succeeded', 'SucceededWithIssues'), or(eq(dependencies.Build.outputs['A.build.NBGV_PublicRelease'], 'True'), ${{parameters.npmDeploy}}))
dependsOn:
- Deploy_NuGet
jobs:
@@ -988,7 +999,10 @@ stages:
- Build
- Build_Docs
- Deploy_NuGet
condition: and(succeeded(), or(eq(dependencies.Build.outputs['A.build.NBGV_PublicRelease'], 'True'), ${{parameters.uploadApiDocs}}))
# Build_Docs must have produced artifacts (we won't upload anything otherwise) and Deploy_NuGet must
# have succeeded — a NuGet failure deliberately blocks the docs upload. Direct result checks avoid
# transitive succeeded()/failed() which would cascade-skip on a MyGet failure.
condition: and(in(dependencies.Build_Docs.result, 'Succeeded', 'SucceededWithIssues'), in(dependencies.Deploy_NuGet.result, 'Succeeded', 'SucceededWithIssues'), or(eq(dependencies.Build.outputs['A.build.NBGV_PublicRelease'], 'True'), ${{parameters.uploadApiDocs}}))
jobs:
- job:
displayName: Upload C# Docs
+5 -5
View File
@@ -117,7 +117,7 @@ stages:
- stage: Integration
displayName: Integration Tests
dependsOn: Build
condition: ${{ eq(parameters.skipIntegrationTests, false) }}
condition: and(succeeded(), ${{ eq(parameters.skipIntegrationTests, false) }})
jobs:
# Integration Tests (SQLite)
- job:
@@ -319,8 +319,8 @@ stages:
- stage: DefaultConfigE2E
displayName: Default Config E2E Tests
dependsOn: Integration
condition: always()
dependsOn: [Build, Integration]
condition: in(dependencies.Build.result, 'Succeeded', 'SucceededWithIssues')
variables:
npm_config_cache: $(Pipeline.Workspace)/.npm_e2e
# Enable console logging in Release mode
@@ -500,8 +500,8 @@ stages:
- stage: AdditionalConfigE2E
displayName: Additional Config E2E Tests
dependsOn: DefaultConfigE2E
condition: always()
dependsOn: [Build, DefaultConfigE2E]
condition: in(dependencies.Build.result, 'Succeeded', 'SucceededWithIssues')
variables:
npm_config_cache: $(Pipeline.Workspace)/.npm_e2e
ASPNETCORE_URLS: https://localhost:44331
+3 -10
View File
@@ -6,16 +6,9 @@ steps:
versionSource: 'fromFile'
versionFilePath: src/Umbraco.Web.UI.Client/.nvmrc
- bash: |
echo "##[command]Install nbgv"
dotnet tool install --tool-path . nbgv
echo "##[command]Running nbgv get-version"
PACKAGE_VERSION=$(nbgv get-version -v NpmPackageVersion)
echo "##[command]Running npm version"
echo "##[debug]Version: $PACKAGE_VERSION"
cd src/Umbraco.Web.UI.Client
npm version $PACKAGE_VERSION --allow-same-version --no-git-tag-version
displayName: Set NPM Version
- template: set-npm-version.yml
parameters:
workingDirectory: src/Umbraco.Web.UI.Client
- task: Cache@2
displayName: Cache node_modules
+15
View File
@@ -0,0 +1,15 @@
parameters:
- name: workingDirectory
type: string
steps:
- bash: |
echo "##[command]Install nbgv"
dotnet tool install --tool-path . nbgv
echo "##[command]Running nbgv get-version"
PACKAGE_VERSION=$(nbgv get-version -v NpmPackageVersion)
echo "##[command]Running npm version"
echo "##[debug]Version: $PACKAGE_VERSION"
cd ${{ parameters.workingDirectory }}
npm version $PACKAGE_VERSION --allow-same-version --no-git-tag-version
displayName: Set NPM Version
@@ -0,0 +1,47 @@
using Microsoft.AspNetCore.Http;
using Umbraco.Cms.Core.DeliveryApi;
using Umbraco.Cms.Core.Models.PublishedContent;
namespace Umbraco.Cms.Api.Delivery.Caching;
/// <summary>
/// Default implementation of <see cref="IDeliveryApiOutputCacheRequestFilter"/> that prevents caching
/// for preview mode requests and requests without public access.
/// </summary>
public class DefaultDeliveryApiOutputCacheRequestFilter : IDeliveryApiOutputCacheRequestFilter
{
private readonly IRequestPreviewService _requestPreviewService;
private readonly IApiAccessService _apiAccessService;
/// <summary>
/// Initializes a new instance of the <see cref="DefaultDeliveryApiOutputCacheRequestFilter"/> class.
/// </summary>
/// <param name="requestPreviewService">The preview service.</param>
/// <param name="apiAccessService">The API access service.</param>
public DefaultDeliveryApiOutputCacheRequestFilter(IRequestPreviewService requestPreviewService, IApiAccessService apiAccessService)
{
_requestPreviewService = requestPreviewService;
_apiAccessService = apiAccessService;
}
/// <inheritdoc />
public virtual bool IsCacheable(HttpContext context)
=> IsPreview() is false && HasPublicAccess();
/// <inheritdoc />
public virtual bool IsCacheable(HttpContext context, IPublishedContent content) => true;
/// <summary>
/// Returns <c>true</c> if the current request is a preview request; <c>false</c> if the request
/// is not a preview and may be cached.
/// </summary>
protected virtual bool IsPreview()
=> _requestPreviewService.IsPreview();
/// <summary>
/// Returns <c>true</c> if the current request has public access; <c>false</c> if the request
/// is not publicly accessible and should not be cached.
/// </summary>
protected virtual bool HasPublicAccess()
=> _apiAccessService.HasPublicAccess();
}
@@ -0,0 +1,17 @@
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Cache;
using Umbraco.Cms.Core.Models.PublishedContent;
namespace Umbraco.Cms.Api.Delivery.Caching;
/// <summary>
/// Tags cached pages for delivery API output caching with their content type alias, enabling eviction by content type.
/// </summary>
internal sealed class DeliveryApiContentTypeOutputCacheTagProvider : IDeliveryApiOutputCacheTagProvider
{
/// <inheritdoc />
public IEnumerable<string> GetTags(IPublishedContent content)
{
yield return Constants.DeliveryApi.OutputCache.ContentTypeTagPrefix + content.ContentType.Alias;
}
}
@@ -0,0 +1,135 @@
using Microsoft.AspNetCore.OutputCaching;
using Microsoft.Extensions.Logging;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Cache;
using Umbraco.Cms.Core.Events;
using Umbraco.Cms.Core.Notifications;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Services.Changes;
using Umbraco.Cms.Core.Sync;
using Umbraco.Cms.Web.Common.Caching;
namespace Umbraco.Cms.Api.Delivery.Caching;
/// <summary>
/// Handles <see cref="ContentCacheRefresherNotification"/> to evict Delivery API output cache entries
/// when content is published, unpublished, moved, or deleted. Also evicts responses for content
/// that references the changed content via picker properties (umbDocument relations).
/// </summary>
internal sealed class DeliveryApiDocumentOutputCacheEvictionHandler
: RelationOutputCacheEvictionHandlerBase, INotificationAsyncHandler<ContentCacheRefresherNotification>
{
private readonly IEnumerable<IDeliveryApiOutputCacheEvictionProvider> _evictionProviders;
private readonly ILogger<DeliveryApiDocumentOutputCacheEvictionHandler> _logger;
/// <summary>
/// Initializes a new instance of the <see cref="DeliveryApiDocumentOutputCacheEvictionHandler"/> class.
/// </summary>
/// <param name="outputCacheStore">The output cache store for evicting cached responses.</param>
/// <param name="relationService">The relation service for querying entity references.</param>
/// <param name="idKeyMap">The ID/key mapping service for converting between integer IDs and GUIDs.</param>
/// <param name="evictionProviders">Custom eviction providers for additional tag-based eviction.</param>
/// <param name="logger">The logger.</param>
public DeliveryApiDocumentOutputCacheEvictionHandler(
IOutputCacheStore outputCacheStore,
IRelationService relationService,
IIdKeyMap idKeyMap,
IEnumerable<IDeliveryApiOutputCacheEvictionProvider> evictionProviders,
ILogger<DeliveryApiDocumentOutputCacheEvictionHandler> logger)
: base(outputCacheStore, relationService, idKeyMap)
{
_evictionProviders = evictionProviders;
_logger = logger;
}
/// <inheritdoc />
public async Task HandleAsync(ContentCacheRefresherNotification notification, CancellationToken cancellationToken)
{
if (notification.MessageType != MessageType.RefreshByPayload
|| notification.MessageObject is not ContentCacheRefresher.JsonPayload[] payloads)
{
return;
}
var changedEntityIds = new List<int>();
foreach (ContentCacheRefresher.JsonPayload payload in payloads)
{
if (payload.Blueprint)
{
continue;
}
await EvictForPayloadAsync(payload, cancellationToken);
changedEntityIds.Add(payload.Id);
}
// Evict content that references the changed content via picker properties.
await EvictRelatedContentAsync(
changedEntityIds,
Constants.Conventions.RelationTypes.RelatedDocumentAlias,
Constants.DeliveryApi.OutputCache.ContentTagPrefix,
_logger,
cancellationToken);
}
private async Task EvictForPayloadAsync(ContentCacheRefresher.JsonPayload payload, CancellationToken cancellationToken)
{
if (payload.ChangeTypes.HasFlag(TreeChangeTypes.RefreshAll))
{
// Evict all Delivery API responses — media responses may reference content via picker properties.
_logger.LogDebug("Content refresh all — evicting all Delivery API output cache entries.");
await OutputCacheStore.EvictByTagAsync(Constants.DeliveryApi.OutputCache.AllTag, cancellationToken);
return;
}
if (payload.Key.HasValue is false)
{
return;
}
Guid contentKey = payload.Key.Value;
if (_logger.IsEnabled(LogLevel.Debug))
{
_logger.LogDebug("Evicting Delivery API output cache for content {ContentKey}.", contentKey);
}
await OutputCacheStore.EvictByTagAsync(Constants.DeliveryApi.OutputCache.ContentTagPrefix + contentKey, cancellationToken);
if (payload.ChangeTypes.HasFlag(TreeChangeTypes.RefreshBranch))
{
if (_logger.IsEnabled(LogLevel.Debug))
{
_logger.LogDebug("Evicting Delivery API output cache for descendants of {ContentKey}.", contentKey);
}
await OutputCacheStore.EvictByTagAsync(Constants.DeliveryApi.OutputCache.AncestorTagPrefix + contentKey, cancellationToken);
}
await InvokeCustomEvictionProvidersAsync(payload, contentKey, cancellationToken);
}
private async Task InvokeCustomEvictionProvidersAsync(ContentCacheRefresher.JsonPayload payload, Guid contentKey, CancellationToken cancellationToken)
{
var context = new OutputCacheContentChangedContext(
payload.Id,
contentKey,
payload.PublishedCultures ?? [],
payload.UnpublishedCultures ?? []);
foreach (IDeliveryApiOutputCacheEvictionProvider provider in _evictionProviders)
{
IEnumerable<string> additionalTags = await provider.GetAdditionalEvictionTagsAsync(context, cancellationToken);
foreach (var tag in additionalTags)
{
if (_logger.IsEnabled(LogLevel.Debug))
{
_logger.LogDebug("Evicting Delivery API output cache tag {Tag} via custom provider.", tag);
}
await OutputCacheStore.EvictByTagAsync(tag, cancellationToken);
}
}
}
}
@@ -0,0 +1,80 @@
using Microsoft.AspNetCore.OutputCaching;
using Microsoft.Extensions.Logging;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Cache;
using Umbraco.Cms.Core.Events;
using Umbraco.Cms.Core.Notifications;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Services.Changes;
using Umbraco.Cms.Core.Sync;
using Umbraco.Cms.Web.Common.Caching;
namespace Umbraco.Cms.Api.Delivery.Caching;
/// <summary>
/// Handles <see cref="MediaCacheRefresherNotification"/> to evict Delivery API output cache entries
/// when media is created, updated, or deleted. Also evicts content responses that reference
/// the changed media via picker properties (umbMedia relations).
/// </summary>
internal sealed class DeliveryApiMediaOutputCacheEvictionHandler
: RelationOutputCacheEvictionHandlerBase, INotificationAsyncHandler<MediaCacheRefresherNotification>
{
private readonly ILogger<DeliveryApiMediaOutputCacheEvictionHandler> _logger;
/// <summary>
/// Initializes a new instance of the <see cref="DeliveryApiMediaOutputCacheEvictionHandler"/> class.
/// </summary>
/// <param name="outputCacheStore">The output cache store for evicting cached responses.</param>
/// <param name="relationService">The relation service for querying entity references.</param>
/// <param name="idKeyMap">The ID/key mapping service for converting between integer IDs and GUIDs.</param>
/// <param name="logger">The logger.</param>
public DeliveryApiMediaOutputCacheEvictionHandler(
IOutputCacheStore outputCacheStore,
IRelationService relationService,
IIdKeyMap idKeyMap,
ILogger<DeliveryApiMediaOutputCacheEvictionHandler> logger)
: base(outputCacheStore, relationService, idKeyMap)
=> _logger = logger;
/// <inheritdoc />
public async Task HandleAsync(MediaCacheRefresherNotification notification, CancellationToken cancellationToken)
{
if (notification.MessageType != MessageType.RefreshByPayload
|| notification.MessageObject is not MediaCacheRefresher.JsonPayload[] payloads)
{
return;
}
foreach (MediaCacheRefresher.JsonPayload payload in payloads)
{
if (payload.ChangeTypes.HasFlag(TreeChangeTypes.RefreshAll))
{
// Evict all Delivery API responses — content responses may include referenced media,
// so evicting only media entries would leave stale media references in content responses.
_logger.LogDebug("Media refresh all — evicting all Delivery API output cache entries.");
await OutputCacheStore.EvictByTagAsync(Constants.DeliveryApi.OutputCache.AllTag, cancellationToken);
return;
}
if (payload.Key.HasValue is false)
{
continue;
}
if (_logger.IsEnabled(LogLevel.Debug))
{
_logger.LogDebug("Evicting Delivery API output cache for media {MediaKey}.", payload.Key.Value);
}
await OutputCacheStore.EvictByTagAsync(Constants.DeliveryApi.OutputCache.MediaTagPrefix + payload.Key.Value, cancellationToken);
}
// Evict content that references the changed media via picker properties.
await EvictRelatedContentAsync(
payloads.Select(p => p.Id),
Constants.Conventions.RelationTypes.RelatedMediaAlias,
Constants.DeliveryApi.OutputCache.ContentTagPrefix,
_logger,
cancellationToken);
}
}
@@ -0,0 +1,54 @@
using Microsoft.AspNetCore.OutputCaching;
using Microsoft.Extensions.Logging;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Cache;
using Umbraco.Cms.Core.Events;
using Umbraco.Cms.Core.Notifications;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Sync;
using Umbraco.Cms.Web.Common.Caching;
namespace Umbraco.Cms.Api.Delivery.Caching;
/// <summary>
/// Handles <see cref="MemberCacheRefresherNotification"/> to evict Delivery API output cache entries
/// for content that references the changed member via picker properties (umbMember relations).
/// </summary>
internal sealed class DeliveryApiMemberOutputCacheEvictionHandler
: RelationOutputCacheEvictionHandlerBase, INotificationAsyncHandler<MemberCacheRefresherNotification>
{
private readonly ILogger<DeliveryApiMemberOutputCacheEvictionHandler> _logger;
/// <summary>
/// Initializes a new instance of the <see cref="DeliveryApiMemberOutputCacheEvictionHandler"/> class.
/// </summary>
/// <param name="outputCacheStore">The output cache store for evicting cached responses.</param>
/// <param name="relationService">The relation service for querying entity references.</param>
/// <param name="idKeyMap">The ID/key mapping service for converting between integer IDs and GUIDs.</param>
/// <param name="logger">The logger.</param>
public DeliveryApiMemberOutputCacheEvictionHandler(
IOutputCacheStore outputCacheStore,
IRelationService relationService,
IIdKeyMap idKeyMap,
ILogger<DeliveryApiMemberOutputCacheEvictionHandler> logger)
: base(outputCacheStore, relationService, idKeyMap)
=> _logger = logger;
/// <inheritdoc />
public async Task HandleAsync(MemberCacheRefresherNotification notification, CancellationToken cancellationToken)
{
if (notification.MessageType != MessageType.RefreshByPayload
|| notification.MessageObject is not MemberCacheRefresher.JsonPayload[] payloads)
{
return;
}
// Evict content that references the changed members via picker properties.
await EvictRelatedContentAsync(
payloads.Select(p => p.Id),
Constants.Conventions.RelationTypes.RelatedMemberAlias,
Constants.DeliveryApi.OutputCache.ContentTagPrefix,
_logger,
cancellationToken);
}
}
@@ -0,0 +1,47 @@
using Microsoft.AspNetCore.OutputCaching;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Primitives;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Models.PublishedContent;
using Umbraco.Cms.Core.Services.Navigation;
namespace Umbraco.Cms.Api.Delivery.Caching;
/// <summary>
/// Output cache policy for Delivery API content endpoints.
/// </summary>
internal sealed class DeliveryApiOutputCacheContentPolicy : DeliveryApiOutputCachePolicyBase
{
/// <summary>
/// Initializes a new instance of the <see cref="DeliveryApiOutputCacheContentPolicy"/> class.
/// </summary>
/// <param name="defaultDuration">The default cache duration from configuration.</param>
/// <param name="defaultVaryByHeaders">The default vary-by headers for content requests.</param>
public DeliveryApiOutputCacheContentPolicy(TimeSpan defaultDuration, StringValues defaultVaryByHeaders)
: base(defaultDuration, defaultVaryByHeaders)
{
}
/// <inheritdoc />
protected override string ResolvedItemsKey => DeliveryApiOutputCacheKeys.ResolvedContentItemsKey;
/// <inheritdoc />
protected override string ItemTagPrefix => Constants.DeliveryApi.OutputCache.ContentTagPrefix;
/// <inheritdoc />
protected override string AllItemsTag => Constants.DeliveryApi.OutputCache.AllContentTag;
/// <inheritdoc />
protected override void AddItemTags(OutputCacheContext context, IPublishedContent item, IServiceProvider services)
{
// Tag with ancestor keys for branch eviction.
IDocumentNavigationQueryService navigationService = services.GetRequiredService<IDocumentNavigationQueryService>();
if (navigationService.TryGetAncestorsKeys(item.Key, out IEnumerable<Guid> ancestorKeys))
{
foreach (Guid ancestorKey in ancestorKeys)
{
context.Tags.Add(Constants.DeliveryApi.OutputCache.AncestorTagPrefix + ancestorKey);
}
}
}
}
@@ -0,0 +1,18 @@
namespace Umbraco.Cms.Api.Delivery.Caching;
/// <summary>
/// Keys used to pass resolved content and media items from controllers to the output cache policy
/// via <see cref="Microsoft.AspNetCore.Http.HttpContext.Items"/>.
/// </summary>
internal static class DeliveryApiOutputCacheKeys
{
/// <summary>
/// Key for storing resolved content items in <see cref="Microsoft.AspNetCore.Http.HttpContext.Items"/>.
/// </summary>
public const string ResolvedContentItemsKey = "Umbraco.DeliveryApi.OutputCache.ResolvedContentItems";
/// <summary>
/// Key for storing resolved media items in <see cref="Microsoft.AspNetCore.Http.HttpContext.Items"/>.
/// </summary>
public const string ResolvedMediaItemsKey = "Umbraco.DeliveryApi.OutputCache.ResolvedMediaItems";
}
@@ -0,0 +1,45 @@
using Microsoft.AspNetCore.OutputCaching;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Cache;
namespace Umbraco.Cms.Api.Delivery.Caching;
/// <summary>
/// Default implementation of <see cref="IDeliveryApiOutputCacheManager"/> that delegates
/// to the ASP.NET Core <see cref="IOutputCacheStore"/>.
/// </summary>
internal sealed class DeliveryApiOutputCacheManager : IDeliveryApiOutputCacheManager
{
private readonly IOutputCacheStore _outputCacheStore;
/// <summary>
/// Initializes a new instance of the <see cref="DeliveryApiOutputCacheManager"/> class.
/// </summary>
/// <param name="outputCacheStore">The ASP.NET Core output cache store.</param>
public DeliveryApiOutputCacheManager(IOutputCacheStore outputCacheStore)
=> _outputCacheStore = outputCacheStore;
/// <inheritdoc />
public async Task EvictContentAsync(Guid contentKey, CancellationToken cancellationToken = default)
=> await _outputCacheStore.EvictByTagAsync(Constants.DeliveryApi.OutputCache.ContentTagPrefix + contentKey, cancellationToken);
/// <inheritdoc />
public async Task EvictMediaAsync(Guid mediaKey, CancellationToken cancellationToken = default)
=> await _outputCacheStore.EvictByTagAsync(Constants.DeliveryApi.OutputCache.MediaTagPrefix + mediaKey, cancellationToken);
/// <inheritdoc />
public async Task EvictByTagAsync(string tag, CancellationToken cancellationToken = default)
=> await _outputCacheStore.EvictByTagAsync(tag, cancellationToken);
/// <inheritdoc />
public async Task EvictAllContentAsync(CancellationToken cancellationToken = default)
=> await _outputCacheStore.EvictByTagAsync(Constants.DeliveryApi.OutputCache.AllContentTag, cancellationToken);
/// <inheritdoc />
public async Task EvictAllMediaAsync(CancellationToken cancellationToken = default)
=> await _outputCacheStore.EvictByTagAsync(Constants.DeliveryApi.OutputCache.AllMediaTag, cancellationToken);
/// <inheritdoc />
public async Task EvictAllAsync(CancellationToken cancellationToken = default)
=> await _outputCacheStore.EvictByTagAsync(Constants.DeliveryApi.OutputCache.AllTag, cancellationToken);
}
@@ -0,0 +1,29 @@
using Microsoft.Extensions.Primitives;
using Umbraco.Cms.Core;
namespace Umbraco.Cms.Api.Delivery.Caching;
/// <summary>
/// Output cache policy for Delivery API media endpoints.
/// </summary>
internal sealed class DeliveryApiOutputCacheMediaPolicy : DeliveryApiOutputCachePolicyBase
{
/// <summary>
/// Initializes a new instance of the <see cref="DeliveryApiOutputCacheMediaPolicy"/> class.
/// </summary>
/// <param name="defaultDuration">The default cache duration from configuration.</param>
/// <param name="defaultVaryByHeaders">The default vary-by headers for media requests.</param>
public DeliveryApiOutputCacheMediaPolicy(TimeSpan defaultDuration, StringValues defaultVaryByHeaders)
: base(defaultDuration, defaultVaryByHeaders)
{
}
/// <inheritdoc />
protected override string ResolvedItemsKey => DeliveryApiOutputCacheKeys.ResolvedMediaItemsKey;
/// <inheritdoc />
protected override string ItemTagPrefix => Constants.DeliveryApi.OutputCache.MediaTagPrefix;
/// <inheritdoc />
protected override string AllItemsTag => Constants.DeliveryApi.OutputCache.AllMediaTag;
}
@@ -1,43 +0,0 @@
using Microsoft.AspNetCore.OutputCaching;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Primitives;
using Umbraco.Cms.Core.DeliveryApi;
namespace Umbraco.Cms.Api.Delivery.Caching;
internal sealed class DeliveryApiOutputCachePolicy : IOutputCachePolicy
{
private readonly TimeSpan _duration;
private readonly StringValues _varyByHeaderNames;
public DeliveryApiOutputCachePolicy(TimeSpan duration, StringValues varyByHeaderNames)
{
_duration = duration;
_varyByHeaderNames = varyByHeaderNames;
}
ValueTask IOutputCachePolicy.CacheRequestAsync(OutputCacheContext context, CancellationToken cancellationToken)
{
IRequestPreviewService requestPreviewService = context
.HttpContext
.RequestServices
.GetRequiredService<IRequestPreviewService>();
IApiAccessService apiAccessService = context
.HttpContext
.RequestServices
.GetRequiredService<IApiAccessService>();
context.EnableOutputCaching = requestPreviewService.IsPreview() is false && apiAccessService.HasPublicAccess();
context.ResponseExpirationTimeSpan = _duration;
context.CacheVaryByRules.HeaderNames = _varyByHeaderNames;
return ValueTask.CompletedTask;
}
ValueTask IOutputCachePolicy.ServeFromCacheAsync(OutputCacheContext context, CancellationToken cancellationToken)
=> ValueTask.CompletedTask;
ValueTask IOutputCachePolicy.ServeResponseAsync(OutputCacheContext context, CancellationToken cancellationToken)
=> ValueTask.CompletedTask;
}
@@ -0,0 +1,154 @@
using Microsoft.AspNetCore.OutputCaching;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Primitives;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Cache;
using Umbraco.Cms.Core.Models.PublishedContent;
namespace Umbraco.Cms.Api.Delivery.Caching;
/// <summary>
/// Base output cache policy for Delivery API endpoints. Handles request filtering, vary-by rules,
/// and tagging. Subclasses specify the resolved-items key, tag prefix, and "all" tag that
/// distinguish content from media.
/// </summary>
internal abstract class DeliveryApiOutputCachePolicyBase : IOutputCachePolicy
{
private readonly TimeSpan _defaultDuration;
private readonly StringValues _defaultVaryByHeaders;
/// <summary>
/// Initializes a new instance of the <see cref="DeliveryApiOutputCachePolicyBase"/> class.
/// </summary>
/// <param name="defaultDuration">The default cache duration from configuration.</param>
/// <param name="defaultVaryByHeaders">The default vary-by headers for this endpoint type.</param>
protected DeliveryApiOutputCachePolicyBase(TimeSpan defaultDuration, StringValues defaultVaryByHeaders)
{
_defaultDuration = defaultDuration;
_defaultVaryByHeaders = defaultVaryByHeaders;
}
/// <summary>
/// Gets the <see cref="Microsoft.AspNetCore.Http.HttpContext.Items"/> key used to retrieve
/// resolved <see cref="IPublishedContent"/> items stashed by the controller.
/// </summary>
protected abstract string ResolvedItemsKey { get; }
/// <summary>
/// Gets the tag prefix for individual item eviction (e.g. <c>umb-dapi-content-</c>).
/// </summary>
protected abstract string ItemTagPrefix { get; }
/// <summary>
/// Gets the "all items" tag for bulk eviction (e.g. <c>umb-dapi-content-all</c>).
/// </summary>
protected abstract string AllItemsTag { get; }
/// <summary>
/// Adds additional per-item tags to the output cache context. Called once per resolved item
/// during <c>ServeResponseAsync</c>. The default implementation does nothing.
/// </summary>
/// <param name="context">The output cache context.</param>
/// <param name="item">The published content or media item.</param>
/// <param name="services">The request service provider.</param>
protected virtual void AddItemTags(OutputCacheContext context, IPublishedContent item, IServiceProvider services)
{
}
/// <inheritdoc />
ValueTask IOutputCachePolicy.CacheRequestAsync(OutputCacheContext context, CancellationToken cancellationToken)
{
IServiceProvider services = context.HttpContext.RequestServices;
ILogger logger = services.GetRequiredService<ILoggerFactory>().CreateLogger(GetType());
IDeliveryApiOutputCacheRequestFilter requestFilter = services.GetRequiredService<IDeliveryApiOutputCacheRequestFilter>();
if (requestFilter.IsCacheable(context.HttpContext) is false)
{
context.EnableOutputCaching = false;
logger.LogDebug("Request filter returned not cacheable — skipping output cache.");
return ValueTask.CompletedTask;
}
context.EnableOutputCaching = true;
context.AllowCacheLookup = true;
context.AllowCacheStorage = true;
context.AllowLocking = true;
context.ResponseExpirationTimeSpan = _defaultDuration;
// Set default vary-by headers.
context.CacheVaryByRules.HeaderNames = _defaultVaryByHeaders;
// Invoke custom vary-by providers (additive, runs after defaults).
IEnumerable<IDeliveryApiOutputCacheVaryByProvider> varyByProviders = services.GetServices<IDeliveryApiOutputCacheVaryByProvider>();
foreach (IDeliveryApiOutputCacheVaryByProvider varyByProvider in varyByProviders)
{
varyByProvider.ConfigureVaryBy(context.HttpContext, context.CacheVaryByRules);
}
// Add base tags for bulk eviction.
context.Tags.Add(AllItemsTag);
context.Tags.Add(Constants.DeliveryApi.OutputCache.AllTag);
return ValueTask.CompletedTask;
}
/// <inheritdoc />
ValueTask IOutputCachePolicy.ServeFromCacheAsync(OutputCacheContext context, CancellationToken cancellationToken)
=> ValueTask.CompletedTask;
/// <inheritdoc />
ValueTask IOutputCachePolicy.ServeResponseAsync(OutputCacheContext context, CancellationToken cancellationToken)
{
if (context.HttpContext.Items[ResolvedItemsKey]
is not IPublishedContent[] items || items.Length == 0)
{
return ValueTask.CompletedTask;
}
IServiceProvider services = context.HttpContext.RequestServices;
ILogger logger = services.GetRequiredService<ILoggerFactory>().CreateLogger(GetType());
IDeliveryApiOutputCacheRequestFilter requestFilter = services.GetRequiredService<IDeliveryApiOutputCacheRequestFilter>();
IEnumerable<IDeliveryApiOutputCacheTagProvider> tagProviders = services.GetServices<IDeliveryApiOutputCacheTagProvider>();
foreach (IPublishedContent item in items)
{
// Check content-aware cacheability.
if (requestFilter.IsCacheable(context.HttpContext, item) is false)
{
context.AllowCacheStorage = false;
if (logger.IsEnabled(LogLevel.Debug))
{
logger.LogDebug("Request filter returned not cacheable for item {ItemKey} — disabling cache storage.", item.Key);
}
return ValueTask.CompletedTask;
}
// Tag with specific item key for targeted eviction.
context.Tags.Add(ItemTagPrefix + item.Key);
// Allow subclasses to add additional per-item tags (e.g. ancestor tags for content).
AddItemTags(context, item, services);
// Invoke custom tag providers.
foreach (IDeliveryApiOutputCacheTagProvider tagProvider in tagProviders)
{
foreach (var tag in tagProvider.GetTags(item))
{
context.Tags.Add(tag);
}
}
}
if (logger.IsEnabled(LogLevel.Debug))
{
logger.LogDebug(
"Caching Delivery API response with {TagCount} tags, duration {Duration}",
context.Tags.Count,
context.ResponseExpirationTimeSpan);
}
return ValueTask.CompletedTask;
}
}
@@ -0,0 +1,38 @@
using Microsoft.AspNetCore.Http;
using Umbraco.Cms.Core.Models.PublishedContent;
namespace Umbraco.Cms.Api.Delivery.Caching;
/// <summary>
/// Determines whether a Delivery API request is eligible for output caching.
/// </summary>
/// <remarks>
/// <para>
/// This interface provides two levels of cacheability checks:
/// </para>
/// <list type="bullet">
/// <item><see cref="IsCacheable(HttpContext)"/> — called before the controller runs, for
/// request-level decisions (e.g. preview mode, access control).</item>
/// <item><see cref="IsCacheable(HttpContext, IPublishedContent)"/> — called after the controller
/// resolves content, for content-aware decisions (e.g. exclude specific content types).</item>
/// </list>
/// </remarks>
public interface IDeliveryApiOutputCacheRequestFilter
{
/// <summary>
/// Gets a value indicating whether the request is eligible for output caching.
/// Called before the controller runs.
/// </summary>
/// <param name="context">The HTTP context for the current request.</param>
/// <returns><c>true</c> if the response may be cached; <c>false</c> to skip caching.</returns>
bool IsCacheable(HttpContext context);
/// <summary>
/// Gets a value indicating whether the response for the given content or media item is eligible
/// for output caching. Called after the controller resolves content.
/// </summary>
/// <param name="context">The HTTP context for the current request.</param>
/// <param name="content">The resolved published content or media item.</param>
/// <returns><c>true</c> if the response may be cached; <c>false</c> to skip caching.</returns>
bool IsCacheable(HttpContext context, IPublishedContent content);
}
@@ -0,0 +1,28 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.OutputCaching;
namespace Umbraco.Cms.Api.Delivery.Caching;
/// <summary>
/// Configures additional vary-by rules for Delivery API output caching.
/// </summary>
/// <remarks>
/// <para>
/// Multiple implementations can be registered; the output cache policy invokes all of them
/// to configure vary-by rules at cache-write time, after the default vary-by headers have been set.
/// </para>
/// <para>
/// Providers have direct access to <see cref="CacheVaryByRules"/> and can configure any aspect
/// including <see cref="CacheVaryByRules.QueryKeys"/>, <see cref="CacheVaryByRules.HeaderNames"/>,
/// and <see cref="CacheVaryByRules.VaryByValues"/>.
/// </para>
/// </remarks>
public interface IDeliveryApiOutputCacheVaryByProvider
{
/// <summary>
/// Configures vary-by rules for the given request.
/// </summary>
/// <param name="context">The HTTP context for the current request.</param>
/// <param name="rules">The vary-by rules to configure.</param>
void ConfigureVaryBy(HttpContext context, CacheVaryByRules rules);
}
@@ -1,14 +0,0 @@
using Microsoft.AspNetCore.Builder;
using Umbraco.Cms.Web.Common.ApplicationBuilder;
namespace Umbraco.Cms.Api.Delivery.Caching;
internal sealed class OutputCachePipelineFilter : UmbracoPipelineFilter
{
public OutputCachePipelineFilter(string name)
: base(name)
=> PostPipeline = PostPipelineAction;
private void PostPipelineAction(IApplicationBuilder applicationBuilder)
=> applicationBuilder.UseOutputCache();
}
@@ -41,17 +41,20 @@ public class ByIdContentApiController : ContentApiItemControllerBase
{
return NotFound();
}
IActionResult? deniedAccessResult = await HandleMemberAccessAsync(contentItem, _requestMemberAccessService).ConfigureAwait(false);
if (deniedAccessResult is not null)
{
return deniedAccessResult;
}
IApiContentResponse? apiContentResponse = ApiContentResponseBuilder.Build(contentItem);
if (apiContentResponse is null)
{
return NotFound();
}
SetOutputCacheContent(contentItem);
return Ok(apiContentResponse);
}
}
@@ -48,6 +48,7 @@ public class ByIdsContentApiController : ContentApiItemControllerBase
.WhereNotNull()
.ToArray();
SetOutputCacheContent(contentItems);
return Ok(apiContentItems);
}
}
@@ -64,6 +64,7 @@ public class ByRouteContentApiController : ContentApiItemControllerBase
return deniedAccessResult;
}
SetOutputCacheContent(contentItem);
return Ok(ApiContentResponseBuilder.Build(contentItem));
}
@@ -2,10 +2,12 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.OutputCaching;
using Umbraco.Cms.Api.Common.Builders;
using Umbraco.Cms.Api.Delivery.Caching;
using Umbraco.Cms.Api.Delivery.Filters;
using Umbraco.Cms.Api.Delivery.Routing;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.DeliveryApi;
using Umbraco.Cms.Core.Models.PublishedContent;
using Umbraco.Cms.Core.Services.OperationStatus;
namespace Umbraco.Cms.Api.Delivery.Controllers.Content;
@@ -50,6 +52,13 @@ public abstract class ContentApiControllerBase : DeliveryApiControllerBase
.Build()),
};
/// <summary>
/// Stores the resolved content items in the HTTP context for use by the output cache policy.
/// </summary>
/// <param name="items">The resolved published content items.</param>
protected void SetOutputCacheContent(params IPublishedContent[] items)
=> HttpContext.Items[DeliveryApiOutputCacheKeys.ResolvedContentItemsKey] = items;
/// <summary>
/// Creates a 403 Forbidden result.
/// </summary>
@@ -62,9 +62,11 @@ public class QueryContentApiController : ContentApiControllerBase
}
PagedModel<Guid> pagedResult = queryAttempt.Result;
IEnumerable<IPublishedContent> contentItems = ApiPublishedContentCache.GetByIds(pagedResult.Items);
IPublishedContent[] contentItems = ApiPublishedContentCache.GetByIds(pagedResult.Items).ToArray();
IApiContentResponse[] apiContentItems = contentItems.Select(ApiContentResponseBuilder.Build).WhereNotNull().ToArray();
SetOutputCacheContent(contentItems);
var model = new PagedViewModel<IApiContentResponse>
{
Total = pagedResult.Total,
@@ -39,6 +39,7 @@ public class ByIdMediaApiController : MediaApiControllerBase
return NotFound();
}
SetOutputCacheMedia(media);
return Ok(BuildApiMediaWithCrops(media));
}
}
@@ -39,6 +39,7 @@ public class ByIdsMediaApiController : MediaApiControllerBase
.Select(BuildApiMediaWithCrops)
.ToArray();
SetOutputCacheMedia(mediaItems);
return Ok(apiMediaItems);
}
}
@@ -43,6 +43,7 @@ public class ByPathMediaApiController : MediaApiControllerBase
return NotFound();
}
SetOutputCacheMedia(media);
return Ok(BuildApiMediaWithCrops(media));
}
}
@@ -1,6 +1,7 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.OutputCaching;
using Umbraco.Cms.Api.Common.Builders;
using Umbraco.Cms.Api.Delivery.Caching;
using Umbraco.Cms.Api.Delivery.Filters;
using Umbraco.Cms.Api.Delivery.Routing;
using Umbraco.Cms.Core;
@@ -33,6 +34,13 @@ public abstract class MediaApiControllerBase : DeliveryApiControllerBase
protected IApiMediaWithCropsResponse BuildApiMediaWithCrops(IPublishedContent media)
=> _apiMediaWithCropsResponseBuilder.Build(media);
/// <summary>
/// Stores the resolved media items in the HTTP context for use by the output cache policy.
/// </summary>
/// <param name="items">The resolved published media items.</param>
protected void SetOutputCacheMedia(params IPublishedContent[] items)
=> HttpContext.Items[DeliveryApiOutputCacheKeys.ResolvedMediaItemsKey] = items;
protected IActionResult ApiMediaQueryOperationStatusResult(ApiMediaQueryOperationStatus status) =>
status switch
{
@@ -59,6 +59,8 @@ public class QueryMediaApiController : MediaApiControllerBase
PagedModel<Guid> pagedResult = queryAttempt.Result;
IPublishedContent[] mediaItems = pagedResult.Items.Select(PublishedMediaCache.GetById).WhereNotNull().ToArray();
SetOutputCacheMedia(mediaItems);
var model = new PagedViewModel<IApiMediaWithCropsResponse>
{
Total = pagedResult.Total,
@@ -5,6 +5,7 @@ using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Primitives;
using Umbraco.Cms.Api.Common.DependencyInjection;
using Umbraco.Cms.Api.Delivery.Accessors;
@@ -18,6 +19,7 @@ using Umbraco.Cms.Api.Delivery.Security;
using Umbraco.Cms.Api.Delivery.Services;
using Umbraco.Cms.Api.Delivery.Services.QueryBuilders;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Cache;
using Umbraco.Cms.Core.Configuration.Models;
using Umbraco.Cms.Core.DeliveryApi;
using Umbraco.Cms.Core.DependencyInjection;
@@ -105,6 +107,10 @@ public static class UmbracoBuilderExtensions
builder.AddNotificationAsyncHandler<MemberDeletedNotification, RevokeMemberAuthenticationTokensNotificationHandler>();
builder.AddNotificationAsyncHandler<AssignedMemberRolesNotification, RevokeMemberAuthenticationTokensNotificationHandler>();
builder.AddNotificationAsyncHandler<RemovedMemberRolesNotification, RevokeMemberAuthenticationTokensNotificationHandler>();
builder.AddNotificationAsyncHandler<ExternalMemberSavedNotification, RevokeMemberAuthenticationTokensNotificationHandler>();
builder.AddNotificationAsyncHandler<ExternalMemberDeletedNotification, RevokeMemberAuthenticationTokensNotificationHandler>();
builder.AddNotificationAsyncHandler<AssignedExternalMemberRolesNotification, RevokeMemberAuthenticationTokensNotificationHandler>();
builder.AddNotificationAsyncHandler<RemovedExternalMemberRolesNotification, RevokeMemberAuthenticationTokensNotificationHandler>();
// FIXME: remove this when Delivery API V1 is removed
builder.Services.AddSingleton<MatcherPolicy, DeliveryApiItemsEndpointsMatcherPolicy>();
@@ -132,7 +138,7 @@ public static class UmbracoBuilderExtensions
{
options.AddPolicy(
Constants.DeliveryApi.OutputCache.ContentCachePolicy,
new DeliveryApiOutputCachePolicy(
new DeliveryApiOutputCacheContentPolicy(
outputCacheSettings.ContentDuration,
new StringValues([Constants.DeliveryApi.HeaderNames.AcceptLanguage, Constants.DeliveryApi.HeaderNames.AcceptSegment, Constants.DeliveryApi.HeaderNames.StartItem])));
}
@@ -141,13 +147,28 @@ public static class UmbracoBuilderExtensions
{
options.AddPolicy(
Constants.DeliveryApi.OutputCache.MediaCachePolicy,
new DeliveryApiOutputCachePolicy(
new DeliveryApiOutputCacheMediaPolicy(
outputCacheSettings.MediaDuration,
Constants.DeliveryApi.HeaderNames.StartItem));
}
});
builder.Services.Configure<UmbracoPipelineOptions>(options => options.AddFilter(new OutputCachePipelineFilter("UmbracoDeliveryApiOutputCache")));
// Register eviction handlers.
builder.AddNotificationAsyncHandler<ContentCacheRefresherNotification, DeliveryApiDocumentOutputCacheEvictionHandler>();
builder.AddNotificationAsyncHandler<MediaCacheRefresherNotification, DeliveryApiMediaOutputCacheEvictionHandler>();
builder.AddNotificationAsyncHandler<MemberCacheRefresherNotification, DeliveryApiMemberOutputCacheEvictionHandler>();
// Register extension point default implementations.
builder.Services.AddSingleton<IDeliveryApiOutputCacheTagProvider, DeliveryApiContentTypeOutputCacheTagProvider>();
builder.Services.AddUnique<IDeliveryApiOutputCacheRequestFilter, DefaultDeliveryApiOutputCacheRequestFilter>();
builder.Services.AddUnique<IDeliveryApiOutputCacheManager, DeliveryApiOutputCacheManager>();
// Signal that Umbraco has enabled output caching so the application builder registers
// the output cache middleware. Gated via a marker rather than IOutputCacheStore so that
// applications calling services.AddOutputCache(...) for their own purposes are not
// affected by Umbraco's automatic middleware registration.
builder.Services.TryAddSingleton<IUmbracoManagedOutputCacheMarker, UmbracoManagedOutputCacheMarker>();
return builder;
}
}
@@ -5,6 +5,7 @@ using Umbraco.Cms.Core.Configuration.Models;
using Umbraco.Cms.Core.Events;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Notifications;
using Umbraco.Cms.Core.Security;
using Umbraco.Cms.Core.Services;
namespace Umbraco.Cms.Api.Delivery.Handlers;
@@ -13,7 +14,11 @@ internal sealed class RevokeMemberAuthenticationTokensNotificationHandler
: INotificationAsyncHandler<MemberSavedNotification>,
INotificationAsyncHandler<MemberDeletedNotification>,
INotificationAsyncHandler<AssignedMemberRolesNotification>,
INotificationAsyncHandler<RemovedMemberRolesNotification>
INotificationAsyncHandler<RemovedMemberRolesNotification>,
INotificationAsyncHandler<ExternalMemberSavedNotification>,
INotificationAsyncHandler<ExternalMemberDeletedNotification>,
INotificationAsyncHandler<AssignedExternalMemberRolesNotification>,
INotificationAsyncHandler<RemovedExternalMemberRolesNotification>
{
private readonly IMemberService _memberService;
private readonly IOpenIddictTokenManager _tokenManager;
@@ -80,6 +85,38 @@ internal sealed class RevokeMemberAuthenticationTokensNotificationHandler
}
}
public async Task HandleAsync(ExternalMemberSavedNotification notification, CancellationToken cancellationToken)
{
if (_enabled is false)
{
return;
}
foreach (ExternalMemberIdentity member in notification.SavedEntities.Where(m => m.IsLockedOut || m.IsApproved is false))
{
await RevokeTokensByKeyAsync(member.Key);
}
}
public async Task HandleAsync(ExternalMemberDeletedNotification notification, CancellationToken cancellationToken)
{
if (_enabled is false)
{
return;
}
foreach (ExternalMemberIdentity member in notification.DeletedEntities)
{
await RevokeTokensByKeyAsync(member.Key);
}
}
public async Task HandleAsync(AssignedExternalMemberRolesNotification notification, CancellationToken cancellationToken)
=> await ExternalMemberRolesChangedAsync(notification);
public async Task HandleAsync(RemovedExternalMemberRolesNotification notification, CancellationToken cancellationToken)
=> await ExternalMemberRolesChangedAsync(notification);
private async Task MemberRolesChangedAsync(MemberRolesNotification notification)
{
if (_enabled is false)
@@ -99,4 +136,32 @@ internal sealed class RevokeMemberAuthenticationTokensNotificationHandler
await RevokeTokensAsync(member);
}
}
private async Task ExternalMemberRolesChangedAsync(ExternalMemberRolesNotification notification)
{
if (_enabled is false)
{
return;
}
foreach (Guid memberKey in notification.MemberKeys)
{
await RevokeTokensByKeyAsync(memberKey);
}
}
private async Task RevokeTokensByKeyAsync(Guid memberKey)
{
var tokens = await _tokenManager.FindBySubjectAsync(memberKey.ToString()).ToArrayAsync();
if (tokens.Any() is false)
{
return;
}
_logger.LogInformation("Revoking {count} active tokens for external member with key {key}", tokens.Length, memberKey);
foreach (var token in tokens)
{
await _tokenManager.DeleteAsync(token);
}
}
}
+2 -2
View File
@@ -26,7 +26,8 @@ RESTful API for Umbraco backoffice operations. Manages content, media, users, an
- **Validation**: FluentValidation via base controllers
- **Serialization**: System.Text.Json with custom converters
- **Mapping**: Manual presentation factories (no AutoMapper)
- **Patching**: JsonPatch.Net for PATCH operations
- **Patching**: Custom patch engine for PATCH operations (Umbraco.Cms.Api.Management.Patching)
- ⚠️ Legacy JsonPatch.Net support (IJsonPatchService) still available but **obsolete** - scheduled for removal in v19
- **Real-time**: SignalR hubs (`BackofficeHub`, `ServerEventHub`)
- **DI**: Microsoft.Extensions.DependencyInjection via `ManagementApiComposer`
@@ -70,7 +71,6 @@ src/Umbraco.Cms.Api.Management/
- **Umbraco.Cms.Api.Common** - Shared API infrastructure (base controllers, OpenAPI config)
- **Umbraco.Infrastructure** - Service implementations, data access
- **Umbraco.PublishedCache.HybridCache** - Published content queries
- **JsonPatch.Net** - JSON Patch (RFC 6902) support
- **Swashbuckle.AspNetCore** - OpenAPI generation
### Design Patterns
@@ -40,10 +40,15 @@ public class BackOfficeLoginController : Controller
/// <param name="cancellationToken">A cancellation token to cancel the operation.</param>
/// <param name="model">The model containing login information and the return URL.</param>
/// <returns>
/// An <see cref="IActionResult"/> that renders the login view with the model, or a bad request result if the return URL is invalid.
/// An <see cref="IActionResult"/> that renders the login view with the model, or a bad request result if the model state or return URL is invalid.
/// </returns>
public async Task<IActionResult> Index(CancellationToken cancellationToken, BackOfficeLoginModel model)
{
if (ModelState.IsValid is false)
{
return BadRequest();
}
AuthenticateResult cookieAuthResult = await HttpContext.AuthenticateAsync(Constants.Security.BackOfficeAuthenticationType);
if (cookieAuthResult.Succeeded)
{
@@ -0,0 +1,75 @@
using Asp.Versioning;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Api.Management.Factories;
using Umbraco.Cms.Api.Management.OperationStatus;
using Umbraco.Cms.Api.Management.Patchers;
using Umbraco.Cms.Api.Management.ViewModels.Document;
using Umbraco.Cms.Api.Management.ViewModels.Patching;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Models.ContentEditing;
using Umbraco.Cms.Core.Security;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Services.OperationStatus;
namespace Umbraco.Cms.Api.Management.Controllers.Document;
[ApiVersion("1.0")]
public class PatchDocumentController : PatchDocumentControllerBase
{
private readonly IContentEditingService _contentEditingService;
private readonly IDocumentPatcher _documentPatcher;
private readonly IDocumentEditingPresentationFactory _presentationFactory;
private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor;
public PatchDocumentController(
IAuthorizationService authorizationService,
IContentEditingService contentEditingService,
IDocumentPatcher documentPatcher,
IDocumentEditingPresentationFactory presentationFactory,
IBackOfficeSecurityAccessor backOfficeSecurityAccessor)
: base(authorizationService)
{
_contentEditingService = contentEditingService;
_documentPatcher = documentPatcher;
_presentationFactory = presentationFactory;
_backOfficeSecurityAccessor = backOfficeSecurityAccessor;
}
[HttpPatch("{id:guid}/patch")]
[MapToApiVersion("1.0")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status422UnprocessableEntity)]
[EndpointSummary("Make partial updates to a document. For more information, see the documentation at https://docs.umbraco.com/umbraco-cms/reference/management-api/patching/document-endpoint-guide or https://docs.umbraco.com/umbraco-cms/reference/management-api/patching/document-endpoint-spec")]
[Consumes("application/json-patch+json")]
public async Task<IActionResult> Patch(
CancellationToken cancellationToken,
Guid id,
PatchDocumentRequestModel requestModel)
=> await HandleRequest(id, async () =>
{
ContentPatchModel patchModel = _presentationFactory.MapPatchModel(requestModel);
// Apply PATCH operations to create an update request model
Attempt<UpdateDocumentRequestModel, ContentPatchingOperationStatus> patchResult =
await _documentPatcher.ApplyPatchAsync(id, patchModel);
if (patchResult.Success is false)
{
return ContentPatchingOperationStatusResult(patchResult.Status);
}
ContentUpdateModel contentUpdateModel = _presentationFactory.MapUpdateModel(patchResult.Result);
// Use the standard update method to save the patched content
Attempt<ContentUpdateResult, ContentEditingOperationStatus> updateResult =
await _contentEditingService.UpdateAsync(id, contentUpdateModel, CurrentUserKey(_backOfficeSecurityAccessor));
return updateResult.Success
? Ok()
: ContentEditingOperationStatusResult(updateResult.Status);
});
}
@@ -0,0 +1,33 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Api.Management.OperationStatus;
namespace Umbraco.Cms.Api.Management.Controllers.Document;
public abstract class PatchDocumentControllerBase : UpdateDocumentControllerBase
{
protected PatchDocumentControllerBase(IAuthorizationService authorizationService)
: base(authorizationService)
{
}
/// <summary>
/// Maps ContentPatchingOperationStatus to appropriate HTTP responses for PATCH operations.
/// </summary>
protected IActionResult ContentPatchingOperationStatusResult(ContentPatchingOperationStatus status)
=> OperationStatusResult(status, problemDetailsBuilder => status switch
{
ContentPatchingOperationStatus.InvalidOperation => BadRequest(problemDetailsBuilder
.WithTitle("Invalid operation")
.WithDetail("One or more PATCH operations were invalid. Check operation structure, path syntax, and operation types.")
.Build()),
ContentPatchingOperationStatus.NotFound => NotFound(problemDetailsBuilder
.WithTitle("The document could not be found")
.Build()),
_ => StatusCode(StatusCodes.Status500InternalServerError, problemDetailsBuilder
.WithTitle("Unknown error")
.WithDetail("An unexpected error occurred during the PATCH operation.")
.Build()),
});
}
@@ -1,3 +1,4 @@
using System.ComponentModel;
using Asp.Versioning;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
@@ -19,6 +20,68 @@ namespace Umbraco.Cms.Api.Management.Controllers.Document.Tree;
[ApiVersion("1.0")]
public class AncestorsDocumentTreeController : DocumentTreeControllerBase
{
/// <summary>
/// Initializes a new instance of the <see cref="AncestorsDocumentTreeController"/> class.
/// </summary>
/// <param name="entityService">Service for managing and retrieving entities in the system.</param>
/// <param name="flagProviders">A collection of providers that supply flags for document tree nodes.</param>
/// <param name="treeFilterService">Service for filtering document tree entities based on user start nodes.</param>
/// <param name="publicAccessService">Service for handling public access permissions on documents.</param>
/// <param name="documentPresentationFactory">Factory for creating document presentation models.</param>
/// <param name="documentPermissionFilterService">Service for filtering documents based on user permissions.</param>
[ActivatorUtilitiesConstructor]
public AncestorsDocumentTreeController(
IEntityService entityService,
FlagProviderCollection flagProviders,
IDocumentStartNodeTreeFilterService treeFilterService,
IPublicAccessService publicAccessService,
IDocumentPresentationFactory documentPresentationFactory,
IDocumentPermissionFilterService documentPermissionFilterService)
: base(
entityService,
flagProviders,
treeFilterService,
publicAccessService,
documentPresentationFactory,
documentPermissionFilterService)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="AncestorsDocumentTreeController"/> class.
/// </summary>
/// <remarks>
/// This constructor exists solely to disambiguate DI container constructor resolution between the new
/// and the existing obsolete constructors; all parameters except those forwarded to the non-obsolete
/// constructor are ignored.
/// </remarks>
/// <param name="entityService">Service for managing and retrieving entities within Umbraco.</param>
/// <param name="flagProviders">A collection of providers that supply flags for document tree nodes.</param>
/// <param name="userStartNodeEntitiesService">Service for resolving user start nodes for entities.</param>
/// <param name="dataTypeService">Service for accessing and managing data types.</param>
/// <param name="treeFilterService">Service for filtering document tree entities based on user start nodes.</param>
/// <param name="publicAccessService">Service for handling public access permissions on documents.</param>
/// <param name="appCaches">Provides access to application-level caches.</param>
/// <param name="backofficeSecurityAccessor">Accessor for backoffice security context and operations.</param>
/// <param name="documentPresentationFactory">Factory for creating document presentation models.</param>
/// <param name="documentPermissionFilterService">Service for filtering documents based on user permissions.</param>
[Obsolete("Please use the non-obsolete constructor. Scheduled for removal in Umbraco 19.")]
[EditorBrowsable(EditorBrowsableState.Never)]
public AncestorsDocumentTreeController(
IEntityService entityService,
FlagProviderCollection flagProviders,
IUserStartNodeEntitiesService userStartNodeEntitiesService,
IDataTypeService dataTypeService,
IDocumentStartNodeTreeFilterService treeFilterService,
IPublicAccessService publicAccessService,
AppCaches appCaches,
IBackOfficeSecurityAccessor backofficeSecurityAccessor,
IDocumentPresentationFactory documentPresentationFactory,
IDocumentPermissionFilterService documentPermissionFilterService)
: this(entityService, flagProviders, treeFilterService, publicAccessService, documentPresentationFactory, documentPermissionFilterService)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="Umbraco.Cms.Api.Management.Controllers.Document.Tree.AncestorsDocumentTreeController"/> class.
/// </summary>
@@ -60,7 +123,7 @@ public class AncestorsDocumentTreeController : DocumentTreeControllerBase
/// <param name="appCaches">Provides access to application-level caches.</param>
/// <param name="backofficeSecurityAccessor">Accessor for backoffice security context and operations.</param>
/// <param name="documentPresentationFactory">Factory for creating document presentation models.</param>
[Obsolete("Please use the constructor taking all parameters. Scheduled for removal in Umbraco 19.")]
[Obsolete("Please use the constructor accepting IDocumentStartNodeTreeFilterService. Scheduled for removal in Umbraco 19.")]
public AncestorsDocumentTreeController(
IEntityService entityService,
FlagProviderCollection flagProviders,
@@ -94,7 +157,7 @@ public class AncestorsDocumentTreeController : DocumentTreeControllerBase
/// <param name="backofficeSecurityAccessor">Accessor for backoffice security context and authentication.</param>
/// <param name="documentPresentationFactory">Factory for creating document presentation models.</param>
/// <param name="documentPermissionFilterService">Service for filtering documents based on user permissions.</param>
[ActivatorUtilitiesConstructor]
[Obsolete("Please use the constructor accepting IDocumentStartNodeTreeFilterService. Scheduled for removal in Umbraco 19.")]
public AncestorsDocumentTreeController(
IEntityService entityService,
FlagProviderCollection flagProviders,
@@ -1,3 +1,4 @@
using System.ComponentModel;
using Asp.Versioning;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
@@ -20,6 +21,68 @@ namespace Umbraco.Cms.Api.Management.Controllers.Document.Tree;
[ApiVersion("1.0")]
public class ChildrenDocumentTreeController : DocumentTreeControllerBase
{
/// <summary>
/// Initializes a new instance of the <see cref="ChildrenDocumentTreeController"/> class.
/// </summary>
/// <param name="entityService">Service for managing and retrieving entities in the system.</param>
/// <param name="flagProviders">A collection of providers that supply flags for document tree nodes.</param>
/// <param name="treeFilterService">Service for filtering document tree entities based on user start nodes.</param>
/// <param name="publicAccessService">Service for handling public access permissions on documents.</param>
/// <param name="documentPresentationFactory">Factory for creating document presentation models.</param>
/// <param name="documentPermissionFilterService">Service for filtering documents based on user permissions.</param>
[ActivatorUtilitiesConstructor]
public ChildrenDocumentTreeController(
IEntityService entityService,
FlagProviderCollection flagProviders,
IDocumentStartNodeTreeFilterService treeFilterService,
IPublicAccessService publicAccessService,
IDocumentPresentationFactory documentPresentationFactory,
IDocumentPermissionFilterService documentPermissionFilterService)
: base(
entityService,
flagProviders,
treeFilterService,
publicAccessService,
documentPresentationFactory,
documentPermissionFilterService)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="ChildrenDocumentTreeController"/> class.
/// </summary>
/// <remarks>
/// This constructor exists solely to disambiguate DI container constructor resolution between the new
/// and the existing obsolete constructors; all parameters except those forwarded to the non-obsolete
/// constructor are ignored.
/// </remarks>
/// <param name="entityService">Service for managing and retrieving entities within Umbraco.</param>
/// <param name="flagProviders">A collection of providers that supply flags for document tree nodes.</param>
/// <param name="userStartNodeEntitiesService">Service for resolving user start nodes for entities.</param>
/// <param name="dataTypeService">Service for accessing and managing data types.</param>
/// <param name="treeFilterService">Service for filtering document tree entities based on user start nodes.</param>
/// <param name="publicAccessService">Service for handling public access permissions on documents.</param>
/// <param name="appCaches">Provides access to application-level caches.</param>
/// <param name="backofficeSecurityAccessor">Accessor for backoffice security context and operations.</param>
/// <param name="documentPresentationFactory">Factory for creating document presentation models.</param>
/// <param name="documentPermissionFilterService">Service for filtering documents based on user permissions.</param>
[Obsolete("Please use the non-obsolete constructor. Scheduled for removal in Umbraco 19.")]
[EditorBrowsable(EditorBrowsableState.Never)]
public ChildrenDocumentTreeController(
IEntityService entityService,
FlagProviderCollection flagProviders,
IUserStartNodeEntitiesService userStartNodeEntitiesService,
IDataTypeService dataTypeService,
IDocumentStartNodeTreeFilterService treeFilterService,
IPublicAccessService publicAccessService,
AppCaches appCaches,
IBackOfficeSecurityAccessor backofficeSecurityAccessor,
IDocumentPresentationFactory documentPresentationFactory,
IDocumentPermissionFilterService documentPermissionFilterService)
: this(entityService, flagProviders, treeFilterService, publicAccessService, documentPresentationFactory, documentPermissionFilterService)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="ChildrenDocumentTreeController"/> class.
/// </summary>
@@ -61,7 +124,7 @@ public class ChildrenDocumentTreeController : DocumentTreeControllerBase
/// <param name="appCaches">Provides application-level caching functionality.</param>
/// <param name="backofficeSecurityAccessor">Accessor for backoffice security context and authentication.</param>
/// <param name="documentPresentationFactory">Factory for creating document presentation models for the API.</param>
[Obsolete("Please use the constructor taking all parameters. Scheduled for removal in Umbraco 19.")]
[Obsolete("Please use the constructor accepting IDocumentStartNodeTreeFilterService. Scheduled for removal in Umbraco 19.")]
public ChildrenDocumentTreeController(
IEntityService entityService,
FlagProviderCollection flagProviders,
@@ -95,7 +158,7 @@ public class ChildrenDocumentTreeController : DocumentTreeControllerBase
/// <param name="backofficeSecurityAccessor">Accessor for backoffice security context.</param>
/// <param name="documentPresentationFactory">Factory for creating document presentation models.</param>
/// <param name="documentPermissionFilterService">Service for filtering documents based on permissions.</param>
[ActivatorUtilitiesConstructor]
[Obsolete("Please use the constructor accepting IDocumentStartNodeTreeFilterService. Scheduled for removal in Umbraco 19.")]
public ChildrenDocumentTreeController(
IEntityService entityService,
FlagProviderCollection flagProviders,
@@ -29,11 +29,14 @@ namespace Umbraco.Cms.Api.Management.Controllers.Document.Tree;
public abstract class DocumentTreeControllerBase : UserStartNodeTreeControllerBase<DocumentTreeItemResponseModel>
{
private readonly IPublicAccessService _publicAccessService;
private readonly AppCaches _appCaches;
private readonly IBackOfficeSecurityAccessor _backofficeSecurityAccessor;
private readonly IDocumentPresentationFactory _documentPresentationFactory;
private readonly IDocumentPermissionFilterService _documentPermissionFilterService;
// Only populated by the obsolete constructor path; used solely by the obsolete
// GetUserStartNodeIds / GetUserStartNodePaths overrides below.
private readonly AppCaches? _appCaches;
private readonly IBackOfficeSecurityAccessor? _backofficeSecurityAccessor;
[Obsolete("Please use the constructor taking all parameters. Scheduled for removal in Umbraco 18.")]
protected DocumentTreeControllerBase(
IEntityService entityService,
@@ -55,7 +58,7 @@ public abstract class DocumentTreeControllerBase : UserStartNodeTreeControllerBa
{
}
[Obsolete("Please use the constructor taking all parameters. Scheduled for removal in Umbraco 19.")]
[Obsolete("Please use the constructor accepting IDocumentStartNodeTreeFilterService. Scheduled for removal in Umbraco 19.")]
protected DocumentTreeControllerBase(
IEntityService entityService,
FlagProviderCollection flagProviders,
@@ -78,7 +81,7 @@ public abstract class DocumentTreeControllerBase : UserStartNodeTreeControllerBa
{
}
[ActivatorUtilitiesConstructor]
[Obsolete("Please use the constructor accepting IDocumentStartNodeTreeFilterService. Scheduled for removal in Umbraco 19.")]
protected DocumentTreeControllerBase(
IEntityService entityService,
FlagProviderCollection flagProviders,
@@ -98,6 +101,30 @@ public abstract class DocumentTreeControllerBase : UserStartNodeTreeControllerBa
_documentPermissionFilterService = documentPermissionFilterService;
}
/// <summary>
/// Initializes a new instance of the <see cref="DocumentTreeControllerBase"/> class.
/// </summary>
/// <param name="entityService">Service for managing and retrieving entities in the system.</param>
/// <param name="flagProviders">A collection of providers that supply flags for document tree nodes.</param>
/// <param name="treeFilterService">Service for filtering document tree entities based on user start nodes.</param>
/// <param name="publicAccessService">Service for handling public access permissions on documents.</param>
/// <param name="documentPresentationFactory">Factory for creating document presentation models.</param>
/// <param name="documentPermissionFilterService">Service for filtering documents based on user permissions.</param>
[ActivatorUtilitiesConstructor]
protected DocumentTreeControllerBase(
IEntityService entityService,
FlagProviderCollection flagProviders,
IDocumentStartNodeTreeFilterService treeFilterService,
IPublicAccessService publicAccessService,
IDocumentPresentationFactory documentPresentationFactory,
IDocumentPermissionFilterService documentPermissionFilterService)
: base(entityService, flagProviders, treeFilterService)
{
_publicAccessService = publicAccessService;
_documentPresentationFactory = documentPresentationFactory;
_documentPermissionFilterService = documentPermissionFilterService;
}
protected override UmbracoObjectTypes ItemObjectType => UmbracoObjectTypes.Document;
protected override Ordering ItemOrdering => Ordering.By(Infrastructure.Persistence.Dtos.NodeDto.SortOrderColumnName);
@@ -122,21 +149,27 @@ public abstract class DocumentTreeControllerBase : UserStartNodeTreeControllerBa
return responseModel;
}
// Only invoked via the CallbackStartNodeTreeFilterService wired up by the obsolete
// UserStartNodeTreeControllerBase constructor. The non-obsolete constructor path
// routes start node resolution through IDocumentStartNodeTreeFilterService and
// never calls these overrides; hence the null-forgiving operator on _appCaches.
/// <inheritdoc/>
[Obsolete("No longer used. Register a custom IDocumentStartNodeTreeFilterService instead. Scheduled for removal in Umbraco 19.")]
protected override int[] GetUserStartNodeIds()
=> _backofficeSecurityAccessor
=> _backofficeSecurityAccessor?
.BackOfficeSecurity?
.CurrentUser?
.CalculateContentStartNodeIds(EntityService, _appCaches)
?? Array.Empty<int>();
.CalculateContentStartNodeIds(EntityService, _appCaches!)
?? [];
/// <inheritdoc/>
[Obsolete("No longer used. Register a custom IDocumentStartNodeTreeFilterService instead. Scheduled for removal in Umbraco 19.")]
protected override string[] GetUserStartNodePaths()
=> _backofficeSecurityAccessor
=> _backofficeSecurityAccessor?
.BackOfficeSecurity?
.CurrentUser?
.GetContentStartNodePaths(EntityService, _appCaches)
?? Array.Empty<string>();
.GetContentStartNodePaths(EntityService, _appCaches!)
?? [];
/// <inheritdoc/>
protected override Task<(IEntitySlim[] Entities, long TotalItems)> FilterTreeEntities(IEntitySlim[] entities, long totalItems)
@@ -1,3 +1,4 @@
using System.ComponentModel;
using Asp.Versioning;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
@@ -20,6 +21,68 @@ namespace Umbraco.Cms.Api.Management.Controllers.Document.Tree;
[ApiVersion("1.0")]
public class RootDocumentTreeController : DocumentTreeControllerBase
{
/// <summary>
/// Initializes a new instance of the <see cref="RootDocumentTreeController"/> class.
/// </summary>
/// <param name="entityService">Service for managing and retrieving entities in the system.</param>
/// <param name="flagProviders">A collection of providers that supply flags for document tree nodes.</param>
/// <param name="treeFilterService">Service for filtering document tree entities based on user start nodes.</param>
/// <param name="publicAccessService">Service for handling public access permissions on documents.</param>
/// <param name="documentPresentationFactory">Factory for creating document presentation models.</param>
/// <param name="documentPermissionFilterService">Service for filtering documents based on user permissions.</param>
[ActivatorUtilitiesConstructor]
public RootDocumentTreeController(
IEntityService entityService,
FlagProviderCollection flagProviders,
IDocumentStartNodeTreeFilterService treeFilterService,
IPublicAccessService publicAccessService,
IDocumentPresentationFactory documentPresentationFactory,
IDocumentPermissionFilterService documentPermissionFilterService)
: base(
entityService,
flagProviders,
treeFilterService,
publicAccessService,
documentPresentationFactory,
documentPermissionFilterService)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="RootDocumentTreeController"/> class.
/// </summary>
/// <remarks>
/// This constructor exists solely to disambiguate DI container constructor resolution between the new
/// and the existing obsolete constructors; all parameters except those forwarded to the non-obsolete
/// constructor are ignored.
/// </remarks>
/// <param name="entityService">Service for managing and retrieving entities within Umbraco.</param>
/// <param name="flagProviders">A collection of providers that supply flags for document tree nodes.</param>
/// <param name="userStartNodeEntitiesService">Service for resolving user start nodes for entities.</param>
/// <param name="dataTypeService">Service for accessing and managing data types.</param>
/// <param name="treeFilterService">Service for filtering document tree entities based on user start nodes.</param>
/// <param name="publicAccessService">Service for handling public access permissions on documents.</param>
/// <param name="appCaches">Provides access to application-level caches.</param>
/// <param name="backofficeSecurityAccessor">Accessor for backoffice security context and operations.</param>
/// <param name="documentPresentationFactory">Factory for creating document presentation models.</param>
/// <param name="documentPermissionFilterService">Service for filtering documents based on user permissions.</param>
[Obsolete("Please use the non-obsolete constructor. Scheduled for removal in Umbraco 19.")]
[EditorBrowsable(EditorBrowsableState.Never)]
public RootDocumentTreeController(
IEntityService entityService,
FlagProviderCollection flagProviders,
IUserStartNodeEntitiesService userStartNodeEntitiesService,
IDataTypeService dataTypeService,
IDocumentStartNodeTreeFilterService treeFilterService,
IPublicAccessService publicAccessService,
AppCaches appCaches,
IBackOfficeSecurityAccessor backofficeSecurityAccessor,
IDocumentPresentationFactory documentPresentationFactory,
IDocumentPermissionFilterService documentPermissionFilterService)
: this(entityService, flagProviders, treeFilterService, publicAccessService, documentPresentationFactory, documentPermissionFilterService)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="RootDocumentTreeController"/> class, which manages the root nodes of the document tree in the Umbraco backoffice.
/// </summary>
@@ -61,7 +124,7 @@ public class RootDocumentTreeController : DocumentTreeControllerBase
/// <param name="appCaches">Provides application-level caching mechanisms.</param>
/// <param name="backofficeSecurityAccessor">Accessor for backoffice security context and operations.</param>
/// <param name="documentPresentationFactory">Factory for creating document presentation models.</param>
[Obsolete("Please use the constructor taking all parameters. Scheduled for removal in Umbraco 19.")]
[Obsolete("Please use the constructor accepting IDocumentStartNodeTreeFilterService. Scheduled for removal in Umbraco 19.")]
public RootDocumentTreeController(
IEntityService entityService,
FlagProviderCollection flagProviders,
@@ -95,7 +158,7 @@ public class RootDocumentTreeController : DocumentTreeControllerBase
/// <param name="backofficeSecurityAccessor">Accessor for backoffice security context and operations.</param>
/// <param name="documentPresentationFactory">Factory for creating document presentation models.</param>
/// <param name="documentPermissionFilterService">Service for filtering documents based on user permissions.</param>
[ActivatorUtilitiesConstructor]
[Obsolete("Please use the constructor accepting IDocumentStartNodeTreeFilterService. Scheduled for removal in Umbraco 19.")]
public RootDocumentTreeController(
IEntityService entityService,
FlagProviderCollection flagProviders,
@@ -1,3 +1,4 @@
using System.ComponentModel;
using Asp.Versioning;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
@@ -20,6 +21,68 @@ namespace Umbraco.Cms.Api.Management.Controllers.Document.Tree;
[ApiVersion("1.0")]
public class SiblingsDocumentTreeController : DocumentTreeControllerBase
{
/// <summary>
/// Initializes a new instance of the <see cref="SiblingsDocumentTreeController"/> class.
/// </summary>
/// <param name="entityService">Service for managing and retrieving entities in the system.</param>
/// <param name="flagProviders">A collection of providers that supply flags for document tree nodes.</param>
/// <param name="treeFilterService">Service for filtering document tree entities based on user start nodes.</param>
/// <param name="publicAccessService">Service for handling public access permissions on documents.</param>
/// <param name="documentPresentationFactory">Factory for creating document presentation models.</param>
/// <param name="documentPermissionFilterService">Service for filtering documents based on user permissions.</param>
[ActivatorUtilitiesConstructor]
public SiblingsDocumentTreeController(
IEntityService entityService,
FlagProviderCollection flagProviders,
IDocumentStartNodeTreeFilterService treeFilterService,
IPublicAccessService publicAccessService,
IDocumentPresentationFactory documentPresentationFactory,
IDocumentPermissionFilterService documentPermissionFilterService)
: base(
entityService,
flagProviders,
treeFilterService,
publicAccessService,
documentPresentationFactory,
documentPermissionFilterService)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="SiblingsDocumentTreeController"/> class.
/// </summary>
/// <remarks>
/// This constructor exists solely to disambiguate DI container constructor resolution between the new
/// and the existing obsolete constructors; all parameters except those forwarded to the non-obsolete
/// constructor are ignored.
/// </remarks>
/// <param name="entityService">Service for managing and retrieving entities within Umbraco.</param>
/// <param name="flagProviders">A collection of providers that supply flags for document tree nodes.</param>
/// <param name="userStartNodeEntitiesService">Service for resolving user start nodes for entities.</param>
/// <param name="dataTypeService">Service for accessing and managing data types.</param>
/// <param name="treeFilterService">Service for filtering document tree entities based on user start nodes.</param>
/// <param name="publicAccessService">Service for handling public access permissions on documents.</param>
/// <param name="appCaches">Provides access to application-level caches.</param>
/// <param name="backofficeSecurityAccessor">Accessor for backoffice security context and operations.</param>
/// <param name="documentPresentationFactory">Factory for creating document presentation models.</param>
/// <param name="documentPermissionFilterService">Service for filtering documents based on user permissions.</param>
[Obsolete("Please use the non-obsolete constructor. Scheduled for removal in Umbraco 19.")]
[EditorBrowsable(EditorBrowsableState.Never)]
public SiblingsDocumentTreeController(
IEntityService entityService,
FlagProviderCollection flagProviders,
IUserStartNodeEntitiesService userStartNodeEntitiesService,
IDataTypeService dataTypeService,
IDocumentStartNodeTreeFilterService treeFilterService,
IPublicAccessService publicAccessService,
AppCaches appCaches,
IBackOfficeSecurityAccessor backofficeSecurityAccessor,
IDocumentPresentationFactory documentPresentationFactory,
IDocumentPermissionFilterService documentPermissionFilterService)
: this(entityService, flagProviders, treeFilterService, publicAccessService, documentPresentationFactory, documentPermissionFilterService)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="SiblingsDocumentTreeController"/> class.
/// </summary>
@@ -61,7 +124,7 @@ public class SiblingsDocumentTreeController : DocumentTreeControllerBase
/// <param name="appCaches">Provides access to application-level caches.</param>
/// <param name="backofficeSecurityAccessor">Accessor for backoffice security context and operations.</param>
/// <param name="documentPresentationFactory">Factory for creating document presentation models.</param>
[Obsolete("Please use the constructor taking all parameters. Scheduled for removal in Umbraco 19.")]
[Obsolete("Please use the constructor accepting IDocumentStartNodeTreeFilterService. Scheduled for removal in Umbraco 19.")]
public SiblingsDocumentTreeController(
IEntityService entityService,
FlagProviderCollection flagProviders,
@@ -95,7 +158,7 @@ public class SiblingsDocumentTreeController : DocumentTreeControllerBase
/// <param name="backofficeSecurityAccessor">Accessor for backoffice security context and user information.</param>
/// <param name="documentPresentationFactory">Factory for creating document presentation models.</param>
/// <param name="documentPermissionFilterService">Service for filtering documents based on user permissions.</param>
[ActivatorUtilitiesConstructor]
[Obsolete("Please use the constructor accepting IDocumentStartNodeTreeFilterService. Scheduled for removal in Umbraco 19.")]
public SiblingsDocumentTreeController(
IEntityService entityService,
FlagProviderCollection flagProviders,
@@ -20,6 +20,9 @@ public abstract class UpdateDocumentControllerBase : DocumentControllerBase
=> _authorizationService = authorizationService;
protected async Task<IActionResult> HandleRequest(Guid id, UpdateDocumentRequestModel requestModel, Func<Task<IActionResult>> authorizedHandler)
=> await HandleRequest(id, authorizedHandler);
protected async Task<IActionResult> HandleRequest(Guid id, Func<Task<IActionResult>> authorizedHandler)
{
// We intentionally don't pass in cultures here.
// This is to support the client sending values for all cultures even if the user doesn't have access to the language.
@@ -1,3 +1,4 @@
using System.ComponentModel;
using Asp.Versioning;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
@@ -18,6 +19,54 @@ namespace Umbraco.Cms.Api.Management.Controllers.Media.Tree;
[ApiVersion("1.0")]
public class AncestorsMediaTreeController : MediaTreeControllerBase
{
/// <summary>
/// Initializes a new instance of the <see cref="AncestorsMediaTreeController"/> class.
/// </summary>
/// <param name="entityService">Service for managing and retrieving entities within the system.</param>
/// <param name="flagProviders">A collection of providers that supply flags for entities.</param>
/// <param name="treeFilterService">Service for filtering media tree entities based on user start nodes.</param>
/// <param name="mediaPresentationFactory">Factory for creating media presentation models.</param>
[ActivatorUtilitiesConstructor]
public AncestorsMediaTreeController(
IEntityService entityService,
FlagProviderCollection flagProviders,
IMediaStartNodeTreeFilterService treeFilterService,
IMediaPresentationFactory mediaPresentationFactory)
: base(entityService, flagProviders, treeFilterService, mediaPresentationFactory)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="AncestorsMediaTreeController"/> class.
/// </summary>
/// <remarks>
/// This constructor exists solely to disambiguate DI container constructor resolution between the new
/// and the existing obsolete constructors; all parameters except those forwarded to the non-obsolete
/// constructor are ignored.
/// </remarks>
/// <param name="entityService">Service for accessing and managing entities within the system.</param>
/// <param name="flagProviders">A collection of providers that supply flags for entities.</param>
/// <param name="userStartNodeEntitiesService">Service for resolving user start nodes for entities.</param>
/// <param name="dataTypeService">Service for accessing and managing data types.</param>
/// <param name="treeFilterService">Service for filtering media tree entities based on user start nodes.</param>
/// <param name="appCaches">Provides access to application-level caches.</param>
/// <param name="backofficeSecurityAccessor">Accessor for backoffice security context and operations.</param>
/// <param name="mediaPresentationFactory">Factory for creating media presentation models.</param>
[Obsolete("Please use the non-obsolete constructor. Scheduled for removal in Umbraco 19.")]
[EditorBrowsable(EditorBrowsableState.Never)]
public AncestorsMediaTreeController(
IEntityService entityService,
FlagProviderCollection flagProviders,
IUserStartNodeEntitiesService userStartNodeEntitiesService,
IDataTypeService dataTypeService,
IMediaStartNodeTreeFilterService treeFilterService,
AppCaches appCaches,
IBackOfficeSecurityAccessor backofficeSecurityAccessor,
IMediaPresentationFactory mediaPresentationFactory)
: this(entityService, flagProviders, treeFilterService, mediaPresentationFactory)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="AncestorsMediaTreeController"/> class.
/// </summary>
@@ -49,7 +98,7 @@ public class AncestorsMediaTreeController : MediaTreeControllerBase
/// <param name="appCaches">Provides access to application-level caches.</param>
/// <param name="backofficeSecurityAccessor">Accessor for backoffice security context and operations.</param>
/// <param name="mediaPresentationFactory">Factory for creating media presentation models.</param>
[ActivatorUtilitiesConstructor]
[Obsolete("Please use the constructor accepting IMediaStartNodeTreeFilterService. Scheduled for removal in Umbraco 19.")]
public AncestorsMediaTreeController(
IEntityService entityService,
FlagProviderCollection flagProviders,
@@ -1,3 +1,4 @@
using System.ComponentModel;
using Asp.Versioning;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
@@ -19,6 +20,54 @@ namespace Umbraco.Cms.Api.Management.Controllers.Media.Tree;
[ApiVersion("1.0")]
public class ChildrenMediaTreeController : MediaTreeControllerBase
{
/// <summary>
/// Initializes a new instance of the <see cref="ChildrenMediaTreeController"/> class.
/// </summary>
/// <param name="entityService">Service for managing and retrieving entities within the system.</param>
/// <param name="flagProviders">A collection of providers that supply flags for entities.</param>
/// <param name="treeFilterService">Service for filtering media tree entities based on user start nodes.</param>
/// <param name="mediaPresentationFactory">Factory for creating media presentation models.</param>
[ActivatorUtilitiesConstructor]
public ChildrenMediaTreeController(
IEntityService entityService,
FlagProviderCollection flagProviders,
IMediaStartNodeTreeFilterService treeFilterService,
IMediaPresentationFactory mediaPresentationFactory)
: base(entityService, flagProviders, treeFilterService, mediaPresentationFactory)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="ChildrenMediaTreeController"/> class.
/// </summary>
/// <remarks>
/// This constructor exists solely to disambiguate DI container constructor resolution between the new
/// and the existing obsolete constructors; all parameters except those forwarded to the non-obsolete
/// constructor are ignored.
/// </remarks>
/// <param name="entityService">Service for accessing and managing entities within the system.</param>
/// <param name="flagProviders">A collection of providers that supply flags for entities.</param>
/// <param name="userStartNodeEntitiesService">Service for resolving user start nodes for entities.</param>
/// <param name="dataTypeService">Service for accessing and managing data types.</param>
/// <param name="treeFilterService">Service for filtering media tree entities based on user start nodes.</param>
/// <param name="appCaches">Provides access to application-level caches.</param>
/// <param name="backofficeSecurityAccessor">Accessor for backoffice security context and operations.</param>
/// <param name="mediaPresentationFactory">Factory for creating media presentation models.</param>
[Obsolete("Please use the non-obsolete constructor. Scheduled for removal in Umbraco 19.")]
[EditorBrowsable(EditorBrowsableState.Never)]
public ChildrenMediaTreeController(
IEntityService entityService,
FlagProviderCollection flagProviders,
IUserStartNodeEntitiesService userStartNodeEntitiesService,
IDataTypeService dataTypeService,
IMediaStartNodeTreeFilterService treeFilterService,
AppCaches appCaches,
IBackOfficeSecurityAccessor backofficeSecurityAccessor,
IMediaPresentationFactory mediaPresentationFactory)
: this(entityService, flagProviders, treeFilterService, mediaPresentationFactory)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="ChildrenMediaTreeController"/> class, responsible for handling API requests related to child media items in the media tree.
/// </summary>
@@ -50,7 +99,7 @@ public class ChildrenMediaTreeController : MediaTreeControllerBase
/// <param name="appCaches">Provides access to application-level caches for performance optimization.</param>
/// <param name="backofficeSecurityAccessor">Accessor for backoffice security context, used for authorization and user information.</param>
/// <param name="mediaPresentationFactory">Factory for creating presentation models for media entities.</param>
[ActivatorUtilitiesConstructor]
[Obsolete("Please use the constructor accepting IMediaStartNodeTreeFilterService. Scheduled for removal in Umbraco 19.")]
public ChildrenMediaTreeController(
IEntityService entityService,
FlagProviderCollection flagProviders,
@@ -1,3 +1,4 @@
using System.ComponentModel;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.DependencyInjection;
@@ -26,10 +27,13 @@ namespace Umbraco.Cms.Api.Management.Controllers.Media.Tree;
[Authorize(Policy = AuthorizationPolicies.SectionAccessForMediaTree)]
public class MediaTreeControllerBase : UserStartNodeTreeControllerBase<MediaTreeItemResponseModel>
{
private readonly AppCaches _appCaches;
private readonly IBackOfficeSecurityAccessor _backofficeSecurityAccessor;
private readonly IMediaPresentationFactory _mediaPresentationFactory;
// Only populated by the obsolete constructor path; used solely by the obsolete
// GetUserStartNodeIds / GetUserStartNodePaths overrides below.
private readonly AppCaches? _appCaches;
private readonly IBackOfficeSecurityAccessor? _backofficeSecurityAccessor;
/// <summary>
/// Initializes a new instance of the <see cref="Umbraco.Cms.Api.Management.Controllers.Media.Tree.MediaTreeControllerBase"/> class.
/// </summary>
@@ -68,7 +72,7 @@ public class MediaTreeControllerBase : UserStartNodeTreeControllerBase<MediaTree
/// <param name="appCaches">Provides access to application-level caches.</param>
/// <param name="backofficeSecurityAccessor">Accessor for backoffice security context and operations.</param>
/// <param name="mediaPresentationFactory">Factory for creating media presentation models.</param>
[ActivatorUtilitiesConstructor]
[Obsolete("Please use the constructor accepting IMediaStartNodeTreeFilterService. Scheduled for removal in Umbraco 19.")]
public MediaTreeControllerBase(
IEntityService entityService,
FlagProviderCollection flagProviders,
@@ -77,11 +81,64 @@ public class MediaTreeControllerBase : UserStartNodeTreeControllerBase<MediaTree
AppCaches appCaches,
IBackOfficeSecurityAccessor backofficeSecurityAccessor,
IMediaPresentationFactory mediaPresentationFactory)
: base(entityService, flagProviders, userStartNodeEntitiesService, dataTypeService)
: base(
entityService,
flagProviders,
userStartNodeEntitiesService,
dataTypeService)
{
_mediaPresentationFactory = mediaPresentationFactory;
_appCaches = appCaches;
_backofficeSecurityAccessor = backofficeSecurityAccessor;
}
/// <summary>
/// Initializes a new instance of the <see cref="MediaTreeControllerBase"/> class.
/// </summary>
/// <param name="entityService">Service for accessing and managing entities within the system.</param>
/// <param name="flagProviders">A collection of providers that supply flags for entities.</param>
/// <param name="treeFilterService">Service for filtering media tree entities based on user start nodes.</param>
/// <param name="mediaPresentationFactory">Factory for creating media presentation models.</param>
[ActivatorUtilitiesConstructor]
public MediaTreeControllerBase(
IEntityService entityService,
FlagProviderCollection flagProviders,
IMediaStartNodeTreeFilterService treeFilterService,
IMediaPresentationFactory mediaPresentationFactory)
: base(entityService, flagProviders, treeFilterService) =>
_mediaPresentationFactory = mediaPresentationFactory;
/// <summary>
/// Initializes a new instance of the <see cref="MediaTreeControllerBase"/> class.
/// </summary>
/// <remarks>
/// This constructor is a parameter superset of the new and existing obsolete constructors. It exists
/// solely because <see cref="ActivatorUtilitiesConstructorAttribute"/> is not honoured by the DI
/// <c>CallSiteFactory</c> at <c>ServiceProvider</c> <c>ValidateOnBuild</c> time, which requires an
/// unambiguous single best-match constructor; all parameters except those forwarded to the non-obsolete
/// constructor are ignored.
/// </remarks>
/// <param name="entityService">Service for accessing and managing entities within the system.</param>
/// <param name="flagProviders">A collection of providers that supply flags for entities.</param>
/// <param name="userStartNodeEntitiesService">Service for resolving user start nodes for entities.</param>
/// <param name="dataTypeService">Service for accessing and managing data types.</param>
/// <param name="treeFilterService">Service for filtering media tree entities based on user start nodes.</param>
/// <param name="appCaches">Provides access to application-level caches.</param>
/// <param name="backofficeSecurityAccessor">Accessor for backoffice security context and operations.</param>
/// <param name="mediaPresentationFactory">Factory for creating media presentation models.</param>
[Obsolete("Please use the non-obsolete constructor. Scheduled for removal in Umbraco 19.")]
[EditorBrowsable(EditorBrowsableState.Never)]
public MediaTreeControllerBase(
IEntityService entityService,
FlagProviderCollection flagProviders,
IUserStartNodeEntitiesService userStartNodeEntitiesService,
IDataTypeService dataTypeService,
IMediaStartNodeTreeFilterService treeFilterService,
AppCaches appCaches,
IBackOfficeSecurityAccessor backofficeSecurityAccessor,
IMediaPresentationFactory mediaPresentationFactory)
: this(entityService, flagProviders, treeFilterService, mediaPresentationFactory)
{
}
protected override UmbracoObjectTypes ItemObjectType => UmbracoObjectTypes.Media;
@@ -105,17 +162,23 @@ public class MediaTreeControllerBase : UserStartNodeTreeControllerBase<MediaTree
return responseModel;
}
// Only invoked via the CallbackStartNodeTreeFilterService wired up by the obsolete
// UserStartNodeTreeControllerBase constructor. The non-obsolete constructor path
// routes start node resolution through IMediaStartNodeTreeFilterService and
// never calls these overrides; hence the null-forgiving operator on _appCaches.
[Obsolete("No longer used. Register a custom IMediaStartNodeTreeFilterService instead. Scheduled for removal in Umbraco 19.")]
protected override int[] GetUserStartNodeIds()
=> _backofficeSecurityAccessor
=> _backofficeSecurityAccessor?
.BackOfficeSecurity?
.CurrentUser?
.CalculateMediaStartNodeIds(EntityService, _appCaches)
?? Array.Empty<int>();
.CalculateMediaStartNodeIds(EntityService, _appCaches!)
?? [];
[Obsolete("No longer used. Register a custom IMediaStartNodeTreeFilterService instead. Scheduled for removal in Umbraco 19.")]
protected override string[] GetUserStartNodePaths()
=> _backofficeSecurityAccessor
=> _backofficeSecurityAccessor?
.BackOfficeSecurity?
.CurrentUser?
.GetMediaStartNodePaths(EntityService, _appCaches)
?? Array.Empty<string>();
.GetMediaStartNodePaths(EntityService, _appCaches!)
?? [];
}
@@ -1,3 +1,4 @@
using System.ComponentModel;
using Asp.Versioning;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
@@ -19,6 +20,54 @@ namespace Umbraco.Cms.Api.Management.Controllers.Media.Tree;
[ApiVersion("1.0")]
public class RootMediaTreeController : MediaTreeControllerBase
{
/// <summary>
/// Initializes a new instance of the <see cref="RootMediaTreeController"/> class.
/// </summary>
/// <param name="entityService">Service for managing and retrieving entities in the system.</param>
/// <param name="flagProviders">A collection of providers that supply flags for entities.</param>
/// <param name="treeFilterService">Service for filtering media tree entities based on user start nodes.</param>
/// <param name="mediaPresentationFactory">Factory for creating media presentation models.</param>
[ActivatorUtilitiesConstructor]
public RootMediaTreeController(
IEntityService entityService,
FlagProviderCollection flagProviders,
IMediaStartNodeTreeFilterService treeFilterService,
IMediaPresentationFactory mediaPresentationFactory)
: base(entityService, flagProviders, treeFilterService, mediaPresentationFactory)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="RootMediaTreeController"/> class.
/// </summary>
/// <remarks>
/// This constructor exists solely to disambiguate DI container constructor resolution between the new
/// and the existing obsolete constructors; all parameters except those forwarded to the non-obsolete
/// constructor are ignored.
/// </remarks>
/// <param name="entityService">Service for accessing and managing entities within the system.</param>
/// <param name="flagProviders">A collection of providers that supply flags for entities.</param>
/// <param name="userStartNodeEntitiesService">Service for resolving user start nodes for entities.</param>
/// <param name="dataTypeService">Service for accessing and managing data types.</param>
/// <param name="treeFilterService">Service for filtering media tree entities based on user start nodes.</param>
/// <param name="appCaches">Provides access to application-level caches.</param>
/// <param name="backofficeSecurityAccessor">Accessor for backoffice security context and operations.</param>
/// <param name="mediaPresentationFactory">Factory for creating media presentation models.</param>
[Obsolete("Please use the non-obsolete constructor. Scheduled for removal in Umbraco 19.")]
[EditorBrowsable(EditorBrowsableState.Never)]
public RootMediaTreeController(
IEntityService entityService,
FlagProviderCollection flagProviders,
IUserStartNodeEntitiesService userStartNodeEntitiesService,
IDataTypeService dataTypeService,
IMediaStartNodeTreeFilterService treeFilterService,
AppCaches appCaches,
IBackOfficeSecurityAccessor backofficeSecurityAccessor,
IMediaPresentationFactory mediaPresentationFactory)
: this(entityService, flagProviders, treeFilterService, mediaPresentationFactory)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="RootMediaTreeController"/> class, which manages the root of the media tree in the Umbraco backoffice API.
/// </summary>
@@ -50,7 +99,7 @@ public class RootMediaTreeController : MediaTreeControllerBase
/// <param name="appCaches">Provides access to application-level caches.</param>
/// <param name="backofficeSecurityAccessor">Accessor for backoffice security context and operations.</param>
/// <param name="mediaPresentationFactory">Factory for creating media presentation models.</param>
[ActivatorUtilitiesConstructor]
[Obsolete("Please use the constructor accepting IMediaStartNodeTreeFilterService. Scheduled for removal in Umbraco 19.")]
public RootMediaTreeController(
IEntityService entityService,
FlagProviderCollection flagProviders,
@@ -1,3 +1,4 @@
using System.ComponentModel;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.DependencyInjection;
@@ -18,6 +19,54 @@ namespace Umbraco.Cms.Api.Management.Controllers.Media.Tree;
/// </summary>
public class SiblingsMediaTreeController : MediaTreeControllerBase
{
/// <summary>
/// Initializes a new instance of the <see cref="SiblingsMediaTreeController"/> class.
/// </summary>
/// <param name="entityService">Service for accessing and managing entities within the system.</param>
/// <param name="flagProviders">A collection of providers that supply flags for entities.</param>
/// <param name="treeFilterService">Service for filtering media tree entities based on user start nodes.</param>
/// <param name="mediaPresentationFactory">Factory for creating media presentation models.</param>
[ActivatorUtilitiesConstructor]
public SiblingsMediaTreeController(
IEntityService entityService,
FlagProviderCollection flagProviders,
IMediaStartNodeTreeFilterService treeFilterService,
IMediaPresentationFactory mediaPresentationFactory)
: base(entityService, flagProviders, treeFilterService, mediaPresentationFactory)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="SiblingsMediaTreeController"/> class.
/// </summary>
/// <remarks>
/// This constructor exists solely to disambiguate DI container constructor resolution between the new
/// and the existing obsolete constructors; all parameters except those forwarded to the non-obsolete
/// constructor are ignored.
/// </remarks>
/// <param name="entityService">Service for accessing and managing entities within the system.</param>
/// <param name="flagProviders">A collection of providers that supply flags for entities.</param>
/// <param name="userStartNodeEntitiesService">Service for resolving user start nodes for entities.</param>
/// <param name="dataTypeService">Service for accessing and managing data types.</param>
/// <param name="treeFilterService">Service for filtering media tree entities based on user start nodes.</param>
/// <param name="appCaches">Provides access to application-level caches.</param>
/// <param name="backofficeSecurityAccessor">Accessor for backoffice security context and operations.</param>
/// <param name="mediaPresentationFactory">Factory for creating media presentation models.</param>
[Obsolete("Please use the non-obsolete constructor. Scheduled for removal in Umbraco 19.")]
[EditorBrowsable(EditorBrowsableState.Never)]
public SiblingsMediaTreeController(
IEntityService entityService,
FlagProviderCollection flagProviders,
IUserStartNodeEntitiesService userStartNodeEntitiesService,
IDataTypeService dataTypeService,
IMediaStartNodeTreeFilterService treeFilterService,
AppCaches appCaches,
IBackOfficeSecurityAccessor backofficeSecurityAccessor,
IMediaPresentationFactory mediaPresentationFactory)
: this(entityService, flagProviders, treeFilterService, mediaPresentationFactory)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="SiblingsMediaTreeController"/> class, responsible for handling API requests related to sibling media items in the media tree.
/// </summary>
@@ -49,7 +98,7 @@ public class SiblingsMediaTreeController : MediaTreeControllerBase
/// <param name="appCaches">Provides access to application-level caches for performance optimization.</param>
/// <param name="backofficeSecurityAccessor">Accessor for back office security context, used for authorization and user information.</param>
/// <param name="mediaPresentationFactory">Factory for creating media presentation models for API responses.</param>
[ActivatorUtilitiesConstructor]
[Obsolete("Please use the constructor accepting IMediaStartNodeTreeFilterService. Scheduled for removal in Umbraco 19.")]
public SiblingsMediaTreeController(
IEntityService entityService,
FlagProviderCollection flagProviders,
@@ -1,9 +1,11 @@
using Asp.Versioning;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.DependencyInjection;
using Umbraco.Cms.Api.Management.Factories;
using Umbraco.Cms.Api.Management.Services;
using Umbraco.Cms.Api.Management.ViewModels.Member;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Core.Security;
using Umbraco.Cms.Core.Services;
@@ -16,24 +18,43 @@ namespace Umbraco.Cms.Api.Management.Controllers.Member;
[ApiVersion("1.0")]
public class ByKeyMemberController : MemberControllerBase
{
private readonly IMemberEditingService _memberEditingService;
private readonly IMemberPresentationFactory _memberPresentationFactory;
private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor;
private readonly IMemberPresentationService _memberPresentationService;
// TODO (V19): Remove the unnecessary parameters provided to the constructor.
/// <summary>
/// Initializes a new instance of the <see cref="ByKeyMemberController"/> class, which handles member management operations by member key.
/// Initializes a new instance of the <see cref="ByKeyMemberController"/> class.
/// </summary>
/// <param name="memberEditingService">Service used to perform editing operations on members.</param>
/// <param name="memberPresentationFactory">Factory for creating member presentation models.</param>
/// <param name="backOfficeSecurityAccessor">Accessor for back office security context.</param>
/// <param name="memberPresentationService">Service for resolving members across both content and external stores.</param>
[ActivatorUtilitiesConstructor]
public ByKeyMemberController(
IMemberEditingService memberEditingService,
IMemberPresentationFactory memberPresentationFactory,
IBackOfficeSecurityAccessor backOfficeSecurityAccessor,
IMemberPresentationService memberPresentationService)
{
_backOfficeSecurityAccessor = backOfficeSecurityAccessor;
_memberPresentationService = memberPresentationService;
}
/// <summary>
/// Initializes a new instance of the <see cref="ByKeyMemberController"/> class.
/// </summary>
[Obsolete("Please use the constructor with all parameters. Scheduled for removal in Umbraco 19.")]
public ByKeyMemberController(
IMemberEditingService memberEditingService,
IMemberPresentationFactory memberPresentationFactory,
IBackOfficeSecurityAccessor backOfficeSecurityAccessor)
: this(
memberEditingService,
memberPresentationFactory,
backOfficeSecurityAccessor,
StaticServiceProvider.Instance.GetRequiredService<IMemberPresentationService>())
{
_memberEditingService = memberEditingService;
_memberPresentationFactory = memberPresentationFactory;
_backOfficeSecurityAccessor = backOfficeSecurityAccessor;
}
/// <summary>
@@ -52,13 +73,7 @@ public class ByKeyMemberController : MemberControllerBase
[EndpointDescription("Gets a member identified by the provided Id.")]
public async Task<IActionResult> ByKey(CancellationToken cancellationToken, Guid id)
{
IMember? member = await _memberEditingService.GetAsync(id);
if (member == null)
{
return MemberNotFound();
}
MemberResponseModel model = await _memberPresentationFactory.CreateResponseModelAsync(member, CurrentUser(_backOfficeSecurityAccessor));
return Ok(model);
MemberResponseModel? model = await _memberPresentationService.CreateResponseModelByKeyAsync(id, CurrentUser(_backOfficeSecurityAccessor));
return model is not null ? Ok(model) : MemberNotFound();
}
}
@@ -19,11 +19,13 @@ public class DeleteMemberController : MemberControllerBase
private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor;
/// <summary>
/// Initializes a new instance of the <see cref="DeleteMemberController"/> class, which handles member deletion operations.
/// Initializes a new instance of the <see cref="DeleteMemberController"/> class.
/// </summary>
/// <param name="memberEditingService">Service used to perform member editing and deletion operations.</param>
/// <param name="backOfficeSecurityAccessor">Accessor for back office security context and authorization.</param>
public DeleteMemberController(IMemberEditingService memberEditingService, IBackOfficeSecurityAccessor backOfficeSecurityAccessor)
public DeleteMemberController(
IMemberEditingService memberEditingService,
IBackOfficeSecurityAccessor backOfficeSecurityAccessor)
{
_memberEditingService = memberEditingService;
_backOfficeSecurityAccessor = backOfficeSecurityAccessor;
@@ -1,10 +1,15 @@
// Copyright (c) Umbraco.
// See LICENSE for more details.
using Asp.Versioning;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.DependencyInjection;
using Umbraco.Cms.Api.Common.ViewModels.Pagination;
using Umbraco.Cms.Api.Management.Factories;
using Umbraco.Cms.Api.Management.ViewModels.Member;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.Membership;
using Umbraco.Cms.Core.Security;
@@ -19,40 +24,48 @@ namespace Umbraco.Cms.Api.Management.Controllers.Member.Filter;
[ApiVersion("1.0")]
public class FilterMemberFilterController : MemberFilterControllerBase
{
private readonly IMemberService _memberService;
private readonly IMemberFilterService _memberFilterService;
private readonly IMemberPresentationFactory _memberPresentationFactory;
private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor;
/// <summary>
/// Initializes a new instance of the <see cref="FilterMemberFilterController"/> class.
/// </summary>
/// <param name="memberService">Service used for member management operations.</param>
/// <param name="memberService">Service used for member management operations (unused, retained for DI compatibility).</param>
/// <param name="memberPresentationFactory">Factory responsible for creating member presentation models.</param>
/// <param name="backOfficeSecurityAccessor">Accessor for back office security context and authentication.</param>
/// <param name="backOfficeSecurityAccessor">Accessor for back office security context (unused, retained for DI compatibility).</param>
/// <param name="memberFilterService">Service for combined member filtering across content and external stores.</param>
// TODO (V19): Remove unused parameters which are only here to avoid ambiguous constructor errors.
[ActivatorUtilitiesConstructor]
public FilterMemberFilterController(
IMemberService memberService,
IMemberPresentationFactory memberPresentationFactory,
IBackOfficeSecurityAccessor backOfficeSecurityAccessor,
IMemberFilterService memberFilterService)
{
_memberFilterService = memberFilterService;
_memberPresentationFactory = memberPresentationFactory;
}
/// <summary>
/// Initializes a new instance of the <see cref="FilterMemberFilterController"/> class.
/// </summary>
[Obsolete("Please use the constructor with all parameters. Scheduled for removal in Umbraco 19.")]
public FilterMemberFilterController(
IMemberService memberService,
IMemberPresentationFactory memberPresentationFactory,
IBackOfficeSecurityAccessor backOfficeSecurityAccessor)
: this(
memberService,
memberPresentationFactory,
backOfficeSecurityAccessor,
StaticServiceProvider.Instance.GetRequiredService<IMemberFilterService>())
{
_memberService = memberService;
_memberPresentationFactory = memberPresentationFactory;
_backOfficeSecurityAccessor = backOfficeSecurityAccessor;
}
/// <summary>
/// Retrieves a paged, filtered collection of members based on the specified criteria.
/// Returns both content-based and external-only members in a unified, correctly paginated result.
/// </summary>
/// <param name="cancellationToken">A token to monitor for cancellation requests.</param>
/// <param name="memberTypeId">An optional member type identifier to filter the results.</param>
/// <param name="memberGroupName">An optional member group name to filter the results.</param>
/// <param name="isApproved">An optional value to filter by member approval status.</param>
/// <param name="isLockedOut">An optional value to filter by member lockout status.</param>
/// <param name="orderBy">The field by which to order the results. The default is <c>"username"</c>.</param>
/// <param name="orderDirection">The direction in which to order the results. The default is <see cref="Direction.Ascending"/>.</param>
/// <param name="filter">An optional filter string to search for members.</param>
/// <param name="skip">The number of items to skip for pagination. The default is 0.</param>
/// <param name="take">The number of items to return for pagination. The default is 100.</param>
/// <returns>A task representing the asynchronous operation. The task result contains an <see cref="IActionResult"/> with a <see cref="PagedViewModel{MemberResponseModel}"/> representing the filtered members.</returns>
[HttpGet]
[MapToApiVersion("1.0")]
[ProducesResponseType(typeof(PagedViewModel<MemberResponseModel>), StatusCodes.Status200OK)]
@@ -71,7 +84,7 @@ public class FilterMemberFilterController : MemberFilterControllerBase
int skip = 0,
int take = 100)
{
var memberFilter = new MemberFilter()
var memberFilter = new MemberFilter
{
MemberTypeId = memberTypeId,
MemberGroupName = memberGroupName,
@@ -80,14 +93,14 @@ public class FilterMemberFilterController : MemberFilterControllerBase
Filter = filter,
};
PagedModel<IMember> members = await _memberService.FilterAsync(memberFilter, orderBy, orderDirection, skip, take);
PagedModel<MemberFilterItem> result = await _memberFilterService.FilterAsync(memberFilter, orderBy, orderDirection, skip, take);
var pageViewModel = new PagedViewModel<MemberResponseModel>
var responseModels = result.Items.Select(_memberPresentationFactory.CreateFilterItemResponseModel).ToList();
return Ok(new PagedViewModel<MemberResponseModel>
{
Items = await _memberPresentationFactory.CreateMultipleAsync(members.Items, CurrentUser(_backOfficeSecurityAccessor)),
Total = members.Total,
};
return Ok(pageViewModel);
Items = responseModels,
Total = result.Total,
});
}
}
@@ -1,11 +1,11 @@
using Asp.Versioning;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.DependencyInjection;
using Umbraco.Cms.Api.Management.Factories;
using Umbraco.Cms.Api.Management.Services;
using Umbraco.Cms.Api.Management.ViewModels.Member.Item;
using Umbraco.Cms.Core.Mapping;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.Entities;
using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Core.Services;
namespace Umbraco.Cms.Api.Management.Controllers.Member.Item;
@@ -17,18 +17,37 @@ namespace Umbraco.Cms.Api.Management.Controllers.Member.Item;
[ApiVersion("1.0")]
public class ItemMemberItemController : MemberItemControllerBase
{
private readonly IEntityService _entityService;
private readonly IMemberPresentationFactory _memberPresentationFactory;
private readonly IMemberPresentationService _memberPresentationService;
// TODO (V19): Remove the unnecessary parameters provided to the constructor.
/// <summary>
/// Initializes a new instance of the <see cref="ItemMemberItemController"/> class, which manages member item operations in the API.
/// Initializes a new instance of the <see cref="ItemMemberItemController"/> class.
/// </summary>
/// <param name="entityService">Service used for entity operations and retrieval.</param>
/// <param name="memberPresentationFactory">Factory responsible for creating member presentation models.</param>
public ItemMemberItemController(IEntityService entityService, IMemberPresentationFactory memberPresentationFactory)
/// <param name="memberPresentationService">Service for resolving members across both content and external stores.</param>
[ActivatorUtilitiesConstructor]
public ItemMemberItemController(
IEntityService entityService,
IMemberPresentationFactory memberPresentationFactory,
IMemberPresentationService memberPresentationService)
{
_memberPresentationService = memberPresentationService;
}
/// <summary>
/// Initializes a new instance of the <see cref="ItemMemberItemController"/> class.
/// </summary>
[Obsolete("Please use the constructor with all parameters. Scheduled for removal in Umbraco 19.")]
public ItemMemberItemController(
IEntityService entityService,
IMemberPresentationFactory memberPresentationFactory)
: this(
entityService,
memberPresentationFactory,
StaticServiceProvider.Instance.GetRequiredService<IMemberPresentationService>())
{
_entityService = entityService;
_memberPresentationFactory = memberPresentationFactory;
}
[HttpGet]
@@ -36,20 +55,16 @@ public class ItemMemberItemController : MemberItemControllerBase
[ProducesResponseType(typeof(IEnumerable<MemberItemResponseModel>), StatusCodes.Status200OK)]
[EndpointSummary("Gets a collection of member items.")]
[EndpointDescription("Gets a collection of member items identified by the provided Ids.")]
public Task<IActionResult> Item(
public async Task<IActionResult> Item(
CancellationToken cancellationToken,
[FromQuery(Name = "id")] HashSet<Guid> ids)
{
if (ids.Count is 0)
{
return Task.FromResult<IActionResult>(Ok(Enumerable.Empty<MemberItemResponseModel>()));
return Ok(Enumerable.Empty<MemberItemResponseModel>());
}
IEnumerable<IMemberEntitySlim> members = _entityService
.GetAll(UmbracoObjectTypes.Member, ids.ToArray())
.OfType<IMemberEntitySlim>();
IEnumerable<MemberItemResponseModel> responseModels = members.Select(_memberPresentationFactory.CreateItemResponseModel);
return Task.FromResult<IActionResult>(Ok(responseModels));
IEnumerable<MemberItemResponseModel> responseModels = await _memberPresentationService.CreateItemResponseModelsAsync(ids);
return Ok(responseModels);
}
}
@@ -96,6 +96,15 @@ public class MemberControllerBase : ContentControllerBase
where TContentModelBase : ContentModelBase<MemberValueModel, MemberVariantRequestModel>
=> ContentEditingOperationStatusResult<TContentModelBase, MemberValueModel, MemberVariantRequestModel>(status, requestModel, validationResult);
/// <summary>
/// Returns a 400 Bad Request indicating that external-only members cannot be modified through the Management API.
/// </summary>
protected IActionResult ExternalMemberCannotBeModified()
=> BadRequest(new ProblemDetailsBuilder()
.WithTitle("External member cannot be modified")
.WithDetail("This member is managed by an external provider. Content operations such as create, update, and property editing are not available for external-only members.")
.Build());
private IActionResult MemberNotFound(ProblemDetailsBuilder problemDetailsBuilder) => NotFound(problemDetailsBuilder
.WithTitle("The requested member could not be found")
.Build());
@@ -1,10 +1,13 @@
using Asp.Versioning;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.DependencyInjection;
using Umbraco.Cms.Api.Common.ViewModels.Pagination;
using Umbraco.Cms.Api.Management.Factories;
using Umbraco.Cms.Api.Management.Services;
using Umbraco.Cms.Api.Management.ViewModels.TrackedReferences;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Services.OperationStatus;
@@ -17,20 +20,39 @@ namespace Umbraco.Cms.Api.Management.Controllers.Member.References;
[ApiVersion("1.0")]
public class ReferencedByMemberController : MemberControllerBase
{
private readonly ITrackedReferencesService _trackedReferencesService;
private readonly IRelationTypePresentationFactory _relationTypePresentationFactory;
private readonly IMemberReferenceService _memberReferenceService;
// TODO (V19): Remove the unnecessary parameters provided to the constructor.
/// <summary>
/// Initializes a new instance of the <see cref="ReferencedByMemberController"/> class.
/// </summary>
/// <param name="trackedReferencesService">An implementation of <see cref="ITrackedReferencesService"/> used to manage tracked references.</param>
/// <param name="relationTypePresentationFactory">An implementation of <see cref="IRelationTypePresentationFactory"/> used to create relation type presentations.</param>
/// <param name="memberReferenceService">Service for retrieving paged references to a member.</param>
[ActivatorUtilitiesConstructor]
public ReferencedByMemberController(
ITrackedReferencesService trackedReferencesService,
IRelationTypePresentationFactory relationTypePresentationFactory,
IMemberReferenceService memberReferenceService)
{
_relationTypePresentationFactory = relationTypePresentationFactory;
_memberReferenceService = memberReferenceService;
}
/// <summary>
/// Initializes a new instance of the <see cref="ReferencedByMemberController"/> class.
/// </summary>
[Obsolete("Please use the constructor with all parameters. Scheduled for removal in Umbraco 19.")]
public ReferencedByMemberController(
ITrackedReferencesService trackedReferencesService,
IRelationTypePresentationFactory relationTypePresentationFactory)
: this(
trackedReferencesService,
relationTypePresentationFactory,
StaticServiceProvider.Instance.GetRequiredService<IMemberReferenceService>())
{
_trackedReferencesService = trackedReferencesService;
_relationTypePresentationFactory = relationTypePresentationFactory;
}
/// <summary>
@@ -52,12 +74,12 @@ public class ReferencedByMemberController : MemberControllerBase
int skip = 0,
int take = 20)
{
PagedModel<RelationItemModel> relationItems = await _trackedReferencesService.GetPagedRelationsForItemAsync(id, skip, take, true);
Attempt<PagedModel<RelationItemModel>, GetReferencesOperationStatus> result = await _memberReferenceService.GetPagedReferencesAsync(id, skip, take);
var pagedViewModel = new PagedViewModel<IReferenceResponseModel>
{
Total = relationItems.Total,
Items = await _relationTypePresentationFactory.CreateReferenceResponseModelsAsync(relationItems.Items),
Total = result.Result.Total,
Items = await _relationTypePresentationFactory.CreateReferenceResponseModelsAsync(result.Result.Items),
};
return pagedViewModel;
@@ -87,17 +109,17 @@ public class ReferencedByMemberController : MemberControllerBase
int skip = 0,
int take = 20)
{
Attempt<PagedModel<RelationItemModel>, GetReferencesOperationStatus> relationItemsAttempt = await _trackedReferencesService.GetPagedRelationsForItemAsync(id, UmbracoObjectTypes.Member, skip, take, true);
Attempt<PagedModel<RelationItemModel>, GetReferencesOperationStatus> result = await _memberReferenceService.GetPagedReferencesAsync(id, skip, take);
if (relationItemsAttempt.Success is false)
if (result.Success is false)
{
return GetReferencesOperationStatusResult(relationItemsAttempt.Status);
return GetReferencesOperationStatusResult(result.Status);
}
var pagedViewModel = new PagedViewModel<IReferenceResponseModel>
{
Total = relationItemsAttempt.Result.Total,
Items = await _relationTypePresentationFactory.CreateReferenceResponseModelsAsync(relationItemsAttempt.Result.Items),
Total = result.Result.Total,
Items = await _relationTypePresentationFactory.CreateReferenceResponseModelsAsync(result.Result.Items),
};
return Ok(pagedViewModel);
@@ -22,7 +22,7 @@ public class UpdateMemberController : MemberControllerBase
private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor;
/// <summary>
/// Initializes a new instance of the <see cref="UpdateMemberController"/> class, responsible for handling member update operations in the management API.
/// Initializes a new instance of the <see cref="UpdateMemberController"/> class.
/// </summary>
/// <param name="memberEditingService">Service used to perform member editing operations.</param>
/// <param name="memberEditingPresentationFactory">Factory for creating presentation models related to member editing.</param>
@@ -49,6 +49,13 @@ public class UpdateMemberController : MemberControllerBase
Guid id,
UpdateMemberRequestModel updateRequestModel)
{
// External-only members cannot be updated through this endpoint.
// Their identity data is managed by the external provider.
if (await _memberEditingService.IsExternalMemberAsync(id))
{
return ExternalMemberCannotBeModified();
}
MemberUpdateModel model = _memberEditingPresentationFactory.MapUpdateModel(updateRequestModel);
Attempt<MemberUpdateResult, MemberEditingStatus> result = await _memberEditingService.UpdateAsync(id, model, CurrentUser(_backOfficeSecurityAccessor));
@@ -20,7 +20,7 @@ public class ValidateUpdateMemberController : MemberControllerBase
private readonly IMemberEditingPresentationFactory _memberEditingPresentationFactory;
/// <summary>
/// Initializes a new instance of the <see cref="Umbraco.Cms.Api.Management.Controllers.Member.ValidateUpdateMemberController"/> class.
/// Initializes a new instance of the <see cref="ValidateUpdateMemberController"/> class.
/// </summary>
/// <param name="memberEditingService">The <see cref="IMemberEditingService"/> used for member editing operations.</param>
/// <param name="memberEditingPresentationFactory">The <see cref="IMemberEditingPresentationFactory"/> used to create member editing presentations.</param>
@@ -44,6 +44,12 @@ public class ValidateUpdateMemberController : MemberControllerBase
Guid id,
UpdateMemberRequestModel requestModel)
{
// External-only members cannot be updated through this endpoint.
if (await _memberEditingService.IsExternalMemberAsync(id))
{
return ExternalMemberCannotBeModified();
}
MemberUpdateModel model = _memberEditingPresentationFactory.MapUpdateModel(requestModel);
Attempt<ContentValidationResult, ContentEditingOperationStatus> result = await _memberEditingService.ValidateUpdateAsync(id, model);
@@ -1,11 +1,11 @@
using Asp.Versioning;
using J2N.Collections.Generic.Extensions;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.DependencyInjection;
using Umbraco.Cms.Api.Management.ViewModels.MemberGroup.Item;
using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Core.Mapping;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.Entities;
using Umbraco.Cms.Core.Services;
namespace Umbraco.Cms.Api.Management.Controllers.MemberGroup.Item;
@@ -16,18 +16,30 @@ namespace Umbraco.Cms.Api.Management.Controllers.MemberGroup.Item;
[ApiVersion("1.0")]
public class ItemMemberGroupItemController : MemberGroupItemControllerBase
{
private readonly IEntityService _entityService;
private readonly IUmbracoMapper _mapper;
private readonly IMemberGroupService _memberGroupService;
// TODO (V19): When the obsolete constructor is removed, also remove the unused dependency on IEntityService.
/// <summary>
/// Initializes a new instance of the <see cref="Umbraco.Cms.Api.Management.Controllers.MemberGroup.Item.ItemMemberGroupItemController"/> class, providing services for managing member group items.
/// Initializes a new instance of the <see cref="ItemMemberGroupItemController"/> class.
/// </summary>
/// <param name="entityService">The service used to interact with entities in the Umbraco CMS.</param>
/// <param name="mapper">The mapper used for mapping Umbraco objects.</param>
public ItemMemberGroupItemController(IEntityService entityService, IUmbracoMapper mapper)
/// <param name="memberGroupService">The service used to look up member groups.</param>
[ActivatorUtilitiesConstructor]
public ItemMemberGroupItemController(IEntityService entityService, IUmbracoMapper mapper, IMemberGroupService memberGroupService)
{
_entityService = entityService;
_mapper = mapper;
_memberGroupService = memberGroupService;
}
[Obsolete("Please use the constructor with all parameters. Scheduled for removal in Umbraco 19.")]
public ItemMemberGroupItemController(IEntityService entityService, IUmbracoMapper mapper)
: this(
entityService,
mapper,
StaticServiceProvider.Instance.GetRequiredService<IMemberGroupService>())
{
}
[HttpGet]
@@ -35,17 +47,19 @@ public class ItemMemberGroupItemController : MemberGroupItemControllerBase
[ProducesResponseType(typeof(IEnumerable<MemberGroupItemResponseModel>), StatusCodes.Status200OK)]
[EndpointSummary("Gets a collection of member group items.")]
[EndpointDescription("Gets a collection of member group items identified by the provided Ids.")]
public Task<IActionResult> Item(
public async Task<IActionResult> Item(
CancellationToken cancellationToken,
[FromQuery(Name = "id")] HashSet<Guid> ids)
{
if (ids.Count is 0)
{
return Task.FromResult<IActionResult>(Ok(Enumerable.Empty<MemberGroupItemResponseModel>()));
return Ok(Enumerable.Empty<MemberGroupItemResponseModel>());
}
IEnumerable<IEntitySlim> memberGroups = _entityService.GetAll(UmbracoObjectTypes.MemberGroup, ids.ToArray());
List<MemberGroupItemResponseModel> responseModel = _mapper.MapEnumerable<IEntitySlim, MemberGroupItemResponseModel>(memberGroups);
return Task.FromResult<IActionResult>(Ok(responseModel));
// Resolve via IMemberGroupService so custom implementations are honoured, rather than
// going directly to the entity/repository layer.
IEnumerable<IMemberGroup> memberGroups = await _memberGroupService.GetAsync(ids);
List<MemberGroupItemResponseModel> responseModel = _mapper.MapEnumerable<IMemberGroup, MemberGroupItemResponseModel>(memberGroups);
return Ok(responseModel);
}
}
@@ -28,6 +28,7 @@ public class ConfigurationServerController : ServerControllerBase
private readonly GlobalSettings _globalSettings;
private readonly IBackOfficeExternalLoginProviders _externalLoginProviders;
private readonly IHostingEnvironment _hostingEnvironment;
private readonly SignalRSettings _signalRSettings;
/// <summary>
/// Initializes a new instance of the <see cref="ConfigurationServerController"/> class.
@@ -36,13 +37,38 @@ public class ConfigurationServerController : ServerControllerBase
/// <param name="globalSettings">The global settings options.</param>
/// <param name="externalLoginProviders">The external login providers for back office.</param>
/// <param name="hostingEnvironment">The hosting environment.</param>
/// <param name="signalRSettings">The SignalR settings options.</param>
[ActivatorUtilitiesConstructor]
public ConfigurationServerController(IOptions<SecuritySettings> securitySettings, IOptions<GlobalSettings> globalSettings, IBackOfficeExternalLoginProviders externalLoginProviders, IHostingEnvironment hostingEnvironment)
public ConfigurationServerController(
IOptions<SecuritySettings> securitySettings,
IOptions<GlobalSettings> globalSettings,
IBackOfficeExternalLoginProviders externalLoginProviders,
IHostingEnvironment hostingEnvironment,
IOptions<SignalRSettings> signalRSettings)
{
_securitySettings = securitySettings.Value;
_globalSettings = globalSettings.Value;
_externalLoginProviders = externalLoginProviders;
_hostingEnvironment = hostingEnvironment;
_signalRSettings = signalRSettings.Value;
}
/// <summary>
/// Initializes a new instance of the <see cref="Umbraco.Cms.Api.Management.Controllers.Server.ConfigurationServerController"/> class.
/// </summary>
/// <param name="securitySettings">The <see cref="SecuritySettings"/> options.</param>
/// <param name="globalSettings">The <see cref="GlobalSettings"/> options.</param>
/// <param name="externalLoginProviders">The external login providers used for back office authentication.</param>
/// <param name="hostingEnvironment">The hosting environment.</param>
[Obsolete("Please use the constructor with all parameters. Scheduled for removal in Umbraco 19.")]
public ConfigurationServerController(IOptions<SecuritySettings> securitySettings, IOptions<GlobalSettings> globalSettings, IBackOfficeExternalLoginProviders externalLoginProviders, IHostingEnvironment hostingEnvironment)
: this(
securitySettings,
globalSettings,
externalLoginProviders,
hostingEnvironment,
StaticServiceProvider.Instance.GetRequiredService<IOptions<SignalRSettings>>())
{
}
/// <summary>
@@ -78,6 +104,10 @@ public class ConfigurationServerController : ServerControllerBase
VersionCheckPeriod = _globalSettings.VersionCheckPeriod,
AllowLocalLogin = _externalLoginProviders.HasDenyLocalLogin() is false,
UmbracoCssPath = _hostingEnvironment.ToAbsolute(_globalSettings.UmbracoCssPath),
SignalR = new SignalRClientSettingsResponseModel
{
SkipNegotiation = _signalRSettings.ClientShouldSkipNegotiation,
},
};
return Task.FromResult<IActionResult>(Ok(responseModel));
@@ -1,8 +1,12 @@
using Asp.Versioning;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using Umbraco.Cms.Api.Management.ViewModels.Template;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Configuration.Models;
using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Security;
using Umbraco.Cms.Core.Services;
@@ -18,18 +22,34 @@ public class CreateTemplateController : TemplateControllerBase
{
private readonly ITemplateService _templateService;
private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor;
private readonly IOptions<RuntimeSettings> _runtimeSettings;
/// <summary>
/// Initializes a new instance of the <see cref="CreateTemplateController"/> class.
/// </summary>
/// <param name="templateService">An instance of <see cref="ITemplateService"/> used to manage templates.</param>
/// <param name="backOfficeSecurityAccessor">An instance of <see cref="IBackOfficeSecurityAccessor"/> used to access back office security information.</param>
/// <param name="runtimeSettings">The runtime configuration settings.</param>
[ActivatorUtilitiesConstructor]
public CreateTemplateController(
ITemplateService templateService,
IBackOfficeSecurityAccessor backOfficeSecurityAccessor)
IBackOfficeSecurityAccessor backOfficeSecurityAccessor,
IOptions<RuntimeSettings> runtimeSettings)
{
_templateService = templateService;
_backOfficeSecurityAccessor = backOfficeSecurityAccessor;
_runtimeSettings = runtimeSettings;
}
[Obsolete("Use the constructor with all parameters. Scheduled for removal in Umbraco 19.")]
public CreateTemplateController(
ITemplateService templateService,
IBackOfficeSecurityAccessor backOfficeSecurityAccessor)
: this(
templateService,
backOfficeSecurityAccessor,
StaticServiceProvider.Instance.GetRequiredService<IOptions<RuntimeSettings>>())
{
}
/// <summary>
@@ -47,6 +67,11 @@ public class CreateTemplateController : TemplateControllerBase
[EndpointDescription("Creates a new template with the configuration specified in the request model.")]
public async Task<IActionResult> Create(CancellationToken cancellationToken, CreateTemplateRequestModel requestModel)
{
if (_runtimeSettings.Value.Mode == RuntimeMode.Production)
{
return TemplateOperationStatusResult(TemplateOperationStatus.NotAllowedInProductionMode);
}
Attempt<ITemplate, TemplateOperationStatus> result = await _templateService.CreateAsync(
requestModel.Name,
requestModel.Alias,
@@ -1,7 +1,11 @@
using Asp.Versioning;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Configuration.Models;
using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Security;
using Umbraco.Cms.Core.Services;
@@ -17,16 +21,34 @@ public class DeleteTemplateController : TemplateControllerBase
{
private readonly ITemplateService _templateService;
private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor;
private readonly IOptions<RuntimeSettings> _runtimeSettings;
/// <summary>
/// Initializes a new instance of the <see cref="DeleteTemplateController"/> class, responsible for handling template deletion operations.
/// </summary>
/// <param name="templateService">The service used to manage templates.</param>
/// <param name="backOfficeSecurityAccessor">Provides access to back office security features.</param>
public DeleteTemplateController(ITemplateService templateService, IBackOfficeSecurityAccessor backOfficeSecurityAccessor)
/// <param name="runtimeSettings">The runtime configuration settings.</param>
[ActivatorUtilitiesConstructor]
public DeleteTemplateController(
ITemplateService templateService,
IBackOfficeSecurityAccessor backOfficeSecurityAccessor,
IOptions<RuntimeSettings> runtimeSettings)
{
_templateService = templateService;
_backOfficeSecurityAccessor = backOfficeSecurityAccessor;
_runtimeSettings = runtimeSettings;
}
[Obsolete("Use the constructor with all parameters. Scheduled for removal in Umbraco 19.")]
public DeleteTemplateController(
ITemplateService templateService,
IBackOfficeSecurityAccessor backOfficeSecurityAccessor)
: this(
templateService,
backOfficeSecurityAccessor,
StaticServiceProvider.Instance.GetRequiredService<IOptions<RuntimeSettings>>())
{
}
/// <summary>
@@ -44,6 +66,11 @@ public class DeleteTemplateController : TemplateControllerBase
[EndpointDescription("Deletes a template identified by the provided Id.")]
public async Task<IActionResult> Delete(CancellationToken cancellationToken, Guid id)
{
if (_runtimeSettings.Value.Mode == RuntimeMode.Production)
{
return TemplateOperationStatusResult(TemplateOperationStatus.NotAllowedInProductionMode);
}
Attempt<ITemplate?, TemplateOperationStatus> result = await _templateService.DeleteAsync(id, CurrentUserKey(_backOfficeSecurityAccessor));
return result.Success
? Ok()
@@ -1,8 +1,12 @@
using Asp.Versioning;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using Umbraco.Cms.Api.Management.ViewModels.Template;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Configuration.Models;
using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Core.Mapping;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Security;
@@ -20,6 +24,7 @@ public class UpdateTemplateController : TemplateControllerBase
private readonly ITemplateService _templateService;
private readonly IUmbracoMapper _umbracoMapper;
private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor;
private readonly IOptions<RuntimeSettings> _runtimeSettings;
/// <summary>
/// Initializes a new instance of the <see cref="UpdateTemplateController"/> class, which manages update operations for templates in the Umbraco CMS.
@@ -27,14 +32,31 @@ public class UpdateTemplateController : TemplateControllerBase
/// <param name="templateService">Service used to perform operations on templates.</param>
/// <param name="umbracoMapper">Mapper used to convert between domain models and API models.</param>
/// <param name="backOfficeSecurityAccessor">Accessor for back office security context.</param>
/// <param name="runtimeSettings">The runtime configuration settings.</param>
[ActivatorUtilitiesConstructor]
public UpdateTemplateController(
ITemplateService templateService,
IUmbracoMapper umbracoMapper,
IBackOfficeSecurityAccessor backOfficeSecurityAccessor)
IBackOfficeSecurityAccessor backOfficeSecurityAccessor,
IOptions<RuntimeSettings> runtimeSettings)
{
_templateService = templateService;
_umbracoMapper = umbracoMapper;
_backOfficeSecurityAccessor = backOfficeSecurityAccessor;
_runtimeSettings = runtimeSettings;
}
[Obsolete("Use the constructor with all parameters. Scheduled for removal in Umbraco 19.")]
public UpdateTemplateController(
ITemplateService templateService,
IUmbracoMapper umbracoMapper,
IBackOfficeSecurityAccessor backOfficeSecurityAccessor)
: this(
templateService,
umbracoMapper,
backOfficeSecurityAccessor,
StaticServiceProvider.Instance.GetRequiredService<IOptions<RuntimeSettings>>())
{
}
/// <summary>
@@ -62,7 +84,13 @@ public class UpdateTemplateController : TemplateControllerBase
return TemplateNotFound();
}
// In production mode, block updates if the content is being changed.
var existingContent = template.Content;
template = _umbracoMapper.Map(requestModel, template);
if (_runtimeSettings.Value.Mode == RuntimeMode.Production && existingContent != template.Content)
{
return TemplateOperationStatusResult(TemplateOperationStatus.ContentChangeNotAllowedInProductionMode);
}
Attempt<ITemplate, TemplateOperationStatus> result = await _templateService.UpdateAsync(template, CurrentUserKey(_backOfficeSecurityAccessor));
@@ -5,9 +5,9 @@ using Umbraco.Cms.Api.Management.Services.Flags;
using Umbraco.Cms.Api.Management.ViewModels.Tree;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.Entities;
using Umbraco.Cms.Core.Services;
using Umbraco.Extensions;
namespace Umbraco.Cms.Api.Management.Controllers.Tree;
@@ -18,11 +18,8 @@ namespace Umbraco.Cms.Api.Management.Controllers.Tree;
public abstract class UserStartNodeTreeControllerBase<TItem> : EntityTreeControllerBase<TItem>
where TItem : ContentTreeItemResponseModel, new()
{
private readonly IUserStartNodeEntitiesService _userStartNodeEntitiesService;
private readonly IDataTypeService _dataTypeService;
private readonly IUserStartNodeTreeFilterService _treeFilterService;
private int[]? _userStartNodeIds;
private string[]? _userStartNodePaths;
private Dictionary<Guid, bool> _accessMap = new();
private Guid? _dataTypeKey;
@@ -39,117 +36,87 @@ public abstract class UserStartNodeTreeControllerBase<TItem> : EntityTreeControl
{
}
#pragma warning disable CS0618 // Type or member is obsolete
[Obsolete("Please use the constructor accepting IUserStartNodeTreeFilterService. Scheduled for removal in Umbraco 19.")]
protected UserStartNodeTreeControllerBase(
IEntityService entityService,
FlagProviderCollection flagProviders,
IUserStartNodeEntitiesService userStartNodeEntitiesService,
IDataTypeService dataTypeService)
: base(entityService, flagProviders)
{
_userStartNodeEntitiesService = userStartNodeEntitiesService;
_dataTypeService = dataTypeService;
}
=> _treeFilterService = new CallbackStartNodeTreeFilterService(
userStartNodeEntitiesService,
dataTypeService,
GetUserStartNodeIds,
GetUserStartNodePaths,
() => ItemObjectType);
#pragma warning restore CS0618 // Type or member is obsolete
protected abstract int[] GetUserStartNodeIds();
/// <summary>
/// Initializes a new instance of the <see cref="UserStartNodeTreeControllerBase{TItem}"/> class.
/// </summary>
/// <param name="entityService">The entity service.</param>
/// <param name="flagProviders">The flag provider collection.</param>
/// <param name="treeFilterService">The user start node tree filter service.</param>
protected UserStartNodeTreeControllerBase(
IEntityService entityService,
FlagProviderCollection flagProviders,
IUserStartNodeTreeFilterService treeFilterService)
: base(entityService, flagProviders) =>
_treeFilterService = treeFilterService;
protected abstract string[] GetUserStartNodePaths();
/// <summary>
/// Gets the calculated start node IDs for the current user.
/// </summary>
/// <returns>An array of start node IDs.</returns>
[Obsolete("No longer used. Register a custom IUserStartNodeTreeFilterService instead. Scheduled for removal in Umbraco 19.")]
protected virtual int[] GetUserStartNodeIds() => [];
/// <summary>
/// Gets the calculated start node paths for the current user.
/// </summary>
/// <returns>An array of start node paths.</returns>
[Obsolete("No longer used. Register a custom IUserStartNodeTreeFilterService instead. Scheduled for removal in Umbraco 19.")]
protected virtual string[] GetUserStartNodePaths() => [];
/// <summary>
/// Configures the controller to ignore user start nodes for a specific data type.
/// </summary>
/// <param name="dataTypeKey">The data type key, or <c>null</c> to disable.</param>
protected void IgnoreUserStartNodesForDataType(Guid? dataTypeKey) => _dataTypeKey = dataTypeKey;
/// <inheritdoc />
protected override IEntitySlim[] GetPagedRootEntities(int skip, int take, out long totalItems)
=> UserHasRootAccess() || IgnoreUserStartNodes()
=> ShouldBypassStartNodeFiltering()
? base.GetPagedRootEntities(skip, take, out totalItems)
: CalculateAccessMap(() => _userStartNodeEntitiesService.RootUserAccessEntities(ItemObjectType, UserStartNodeIds), out totalItems);
: MapAccessEntities(_treeFilterService.GetFilteredRootEntities(out totalItems));
/// <inheritdoc />
protected override IEntitySlim[] GetPagedChildEntities(Guid parentKey, int skip, int take, out long totalItems)
{
if (UserHasRootAccess() || IgnoreUserStartNodes())
{
return base.GetPagedChildEntities(parentKey, skip, take, out totalItems);
}
IEnumerable<UserAccessEntity> userAccessEntities = _userStartNodeEntitiesService.ChildUserAccessEntities(
ItemObjectType,
UserStartNodePaths,
parentKey,
skip,
take,
ItemOrdering,
out totalItems);
return CalculateAccessMap(() => userAccessEntities, out _);
}
=> ShouldBypassStartNodeFiltering()
? base.GetPagedChildEntities(parentKey, skip, take, out totalItems)
: MapAccessEntities(_treeFilterService.GetFilteredChildEntities(parentKey, skip, take, ItemOrdering, out totalItems));
/// <inheritdoc />
protected override IEntitySlim[] GetSiblingEntities(Guid target, int before, int after, out long totalBefore, out long totalAfter)
{
if (UserHasRootAccess() || IgnoreUserStartNodes())
{
return base.GetSiblingEntities(target, before, after, out totalBefore, out totalAfter);
}
IEnumerable<UserAccessEntity> userAccessEntities = _userStartNodeEntitiesService.SiblingUserAccessEntities(
ItemObjectType,
UserStartNodePaths,
target,
before,
after,
ItemOrdering,
out totalBefore,
out totalAfter);
return CalculateAccessMap(() => userAccessEntities, out _);
}
=> ShouldBypassStartNodeFiltering()
? base.GetSiblingEntities(target, before, after, out totalBefore, out totalAfter)
: MapAccessEntities(_treeFilterService.GetFilteredSiblingEntities(target, before, after, ItemOrdering, out totalBefore, out totalAfter));
/// <inheritdoc />
protected override TItem[] MapTreeItemViewModels(Guid? parentKey, IEntitySlim[] entities)
=> ShouldBypassStartNodeFiltering()
? base.MapTreeItemViewModels(parentKey, entities)
: _treeFilterService.MapWithAccessFiltering(
entities,
_accessMap,
entity => MapTreeItemViewModel(parentKey, entity),
entity => MapTreeItemViewModelAsNoAccess(parentKey, entity));
private IEntitySlim[] MapAccessEntities(UserAccessEntity[] userAccessEntities)
{
if (UserHasRootAccess() || IgnoreUserStartNodes())
{
return base.MapTreeItemViewModels(parentKey, entities);
}
// for users with no root access, only add items for the entities contained within the calculated access map.
// the access map may contain entities that the user does not have direct access to, but need still to see,
// because it has descendants that the user *does* have access to. these entities are added as "no access" items.
TItem[] contentTreeItemViewModels = entities.Select(entity =>
{
if (_accessMap.TryGetValue(entity.Key, out var hasAccess) == false)
{
// entity is not a part of the calculated access map
return null;
}
// direct access => return a regular item
// no direct access => return a "no access" item
return hasAccess
? MapTreeItemViewModel(parentKey, entity)
: MapTreeItemViewModelAsNoAccess(parentKey, entity);
})
.WhereNotNull()
.ToArray();
return contentTreeItemViewModels;
}
private int[] UserStartNodeIds => _userStartNodeIds ??= GetUserStartNodeIds();
private string[] UserStartNodePaths => _userStartNodePaths ??= GetUserStartNodePaths();
private bool UserHasRootAccess() => UserStartNodeIds.Contains(Constants.System.Root);
private bool IgnoreUserStartNodes()
=> _dataTypeKey.HasValue
&& _dataTypeService.IsDataTypeIgnoringUserStartNodes(_dataTypeKey.Value);
private IEntitySlim[] CalculateAccessMap(Func<IEnumerable<UserAccessEntity>> getUserAccessEntities, out long totalItems)
{
UserAccessEntity[] userAccessEntities = getUserAccessEntities().ToArray();
_accessMap = userAccessEntities.ToDictionary(uae => uae.Entity.Key, uae => uae.HasAccess);
IEntitySlim[] entities = userAccessEntities.Select(uae => uae.Entity).ToArray();
totalItems = entities.Length;
return entities;
return userAccessEntities.Select(uae => uae.Entity).ToArray();
}
private TItem MapTreeItemViewModelAsNoAccess(Guid? parentKey, IEntitySlim entity)
@@ -158,4 +125,45 @@ public abstract class UserStartNodeTreeControllerBase<TItem> : EntityTreeControl
viewModel.NoAccess = true;
return viewModel;
}
private bool ShouldBypassStartNodeFiltering()
=> _treeFilterService.ShouldBypassStartNodeFiltering(_dataTypeKey);
/// <summary>
/// A backward-compatible adapter that implements <see cref="UserStartNodeTreeFilterService"/>
/// by delegating start node resolution to callback functions.
/// </summary>
/// <remarks>
/// Used by the obsolete constructor to bridge the old abstract-method-based
/// start node resolution to the new service-based approach.
/// </remarks>
[Obsolete("Only used by the obsolete constructor. Scheduled for removal in Umbraco 19.")]
private sealed class CallbackStartNodeTreeFilterService : UserStartNodeTreeFilterService
{
private readonly Func<int[]> _getStartNodeIds;
private readonly Func<string[]> _getStartNodePaths;
private readonly Func<UmbracoObjectTypes> _getTreeObjectType;
public CallbackStartNodeTreeFilterService(
IUserStartNodeEntitiesService userStartNodeEntitiesService,
IDataTypeService dataTypeService,
Func<int[]> getStartNodeIds,
Func<string[]> getStartNodePaths,
Func<UmbracoObjectTypes> getTreeObjectType)
: base(userStartNodeEntitiesService, dataTypeService)
{
_getStartNodeIds = getStartNodeIds;
_getStartNodePaths = getStartNodePaths;
_getTreeObjectType = getTreeObjectType;
}
/// <inheritdoc />
protected override UmbracoObjectTypes TreeObjectType => _getTreeObjectType();
/// <inheritdoc />
protected override int[] CalculateUserStartNodeIds() => _getStartNodeIds();
/// <inheritdoc />
protected override string[] CalculateUserStartNodePaths() => _getStartNodePaths();
}
}
@@ -0,0 +1,54 @@
using Asp.Versioning;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Core.Security;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Services.OperationStatus;
namespace Umbraco.Cms.Api.Management.Controllers.User.Current;
/// <summary>
/// Controller responsible for handling requests to clear the avatar of the currently authenticated user.
/// </summary>
[ApiVersion("1.0")]
public class ClearAvatarCurrentUserController : CurrentUserControllerBase
{
private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor;
private readonly IUserService _userService;
/// <summary>
/// Initializes a new instance of the <see cref="ClearAvatarCurrentUserController"/> class.
/// </summary>
/// <param name="backOfficeSecurityAccessor">Provides access to back office security features for the current user.</param>
/// <param name="userService">Service for managing user-related operations.</param>
public ClearAvatarCurrentUserController(
IBackOfficeSecurityAccessor backOfficeSecurityAccessor,
IUserService userService)
{
_backOfficeSecurityAccessor = backOfficeSecurityAccessor;
_userService = userService;
}
/// <summary>
/// Removes the avatar image for the currently authenticated user.
/// </summary>
/// <param name="cancellationToken">A cancellation token to cancel the operation.</param>
/// <returns>An <see cref="IActionResult"/> indicating the result of the operation.</returns>
[MapToApiVersion("1.0")]
[HttpDelete("avatar")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
[EndpointSummary("Clears the current user's avatar.")]
[EndpointDescription("Removes the avatar image for the currently authenticated user.")]
public async Task<IActionResult> ClearAvatar(CancellationToken cancellationToken)
{
Guid userKey = CurrentUserKey(_backOfficeSecurityAccessor);
UserOperationStatus result = await _userService.ClearAvatarAsync(userKey);
return result is UserOperationStatus.Success
? Ok()
: UserOperationStatusResult(result);
}
}
@@ -59,7 +59,7 @@ public class GetCurrentUserController : CurrentUserControllerBase
[EndpointDescription("Gets the currently authenticated back office user's information and permissions.")]
public async Task<IActionResult> GetCurrentUser(CancellationToken cancellationToken)
{
var currentUserKey = CurrentUserKey(_backOfficeSecurityAccessor);
Guid currentUserKey = CurrentUserKey(_backOfficeSecurityAccessor);
AuthorizationResult authorizationResult = await _authorizationService.AuthorizeResourceAsync(
User,
@@ -78,7 +78,7 @@ public class GetCurrentUserController : CurrentUserControllerBase
return Unauthorized();
}
var responseModel = await _userPresentationFactory.CreateCurrentUserResponseModelAsync(user);
CurrentUserResponseModel responseModel = await _userPresentationFactory.CreateCurrentUserResponseModelAsync(user);
return Ok(responseModel);
}
}
@@ -1,10 +1,12 @@
using Asp.Versioning;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.DependencyInjection;
using Umbraco.Cms.Api.Management.ViewModels.User.Current;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Core.Mapping;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.Membership;
using Umbraco.Cms.Core.Security;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Services.OperationStatus;
@@ -18,23 +20,46 @@ namespace Umbraco.Cms.Api.Management.Controllers.User.Current;
public class GetDocumentPermissionsCurrentUserController : CurrentUserControllerBase
{
private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor;
private readonly IUserService _userService;
private readonly IUmbracoMapper _mapper;
private readonly IContentPermissionService _contentPermissionService;
// TODO (V19): Remove the IUserService parameter from the constructor as it is not used in the current implementation.
/// <summary>
/// Initializes a new instance of the <see cref="GetDocumentPermissionsCurrentUserController"/> class, which handles requests related to retrieving document permissions for the current user.
/// </summary>
/// <param name="backOfficeSecurityAccessor">Provides access to back office security information for the current user.</param>
/// <param name="mapper">The Umbraco object mapper used for mapping between models.</param>
/// <param name="contentPermissionService">Service for managing content permissions.</param>
[ActivatorUtilitiesConstructor]
public GetDocumentPermissionsCurrentUserController(
IBackOfficeSecurityAccessor backOfficeSecurityAccessor,
IUserService userService,
IUmbracoMapper mapper,
IContentPermissionService contentPermissionService)
{
_backOfficeSecurityAccessor = backOfficeSecurityAccessor;
_mapper = mapper;
_contentPermissionService = contentPermissionService;
}
/// <summary>
/// Initializes a new instance of the <see cref="GetDocumentPermissionsCurrentUserController"/> class.
/// </summary>
/// <param name="backOfficeSecurityAccessor">Provides access to back office security information for the current user.</param>
/// <param name="userService">Service for managing and retrieving user information.</param>
/// <param name="mapper">The Umbraco object mapper used for mapping between models.</param>
[Obsolete("Please use the constructor with all parameters. Scheduled for removal in Umbraco 19.")]
public GetDocumentPermissionsCurrentUserController(
IBackOfficeSecurityAccessor backOfficeSecurityAccessor,
IUserService userService,
IUmbracoMapper mapper)
: this(
backOfficeSecurityAccessor,
userService,
mapper,
StaticServiceProvider.Instance.GetRequiredService<IContentPermissionService>())
{
_backOfficeSecurityAccessor = backOfficeSecurityAccessor;
_userService = userService;
_mapper = mapper;
}
/// <summary>
@@ -42,10 +67,10 @@ public class GetDocumentPermissionsCurrentUserController : CurrentUserController
/// </summary>
/// <param name="cancellationToken">A token to monitor for cancellation requests.</param>
/// <param name="ids">A set of document IDs for which to retrieve permissions.</param>
/// <returns>An <see cref="IActionResult"/> containing a <see cref="UserPermissionsResponseModel"/> with the permissions for each requested document, or a <see cref="ProblemDetails"/> if not found.</returns>
/// <returns>An <see cref="IActionResult"/> containing a <see cref="UserPermissionsResponseModel"/> with the permissions for each requested document.</returns>
[MapToApiVersion("1.0")]
[HttpGet("permissions/document")]
[ProducesResponseType(typeof(IEnumerable<UserPermissionsResponseModel>), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(UserPermissionsResponseModel), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
[EndpointSummary("Gets document permissions for the current user.")]
[EndpointDescription("Gets the document permissions for the currently authenticated user.")]
@@ -53,14 +78,16 @@ public class GetDocumentPermissionsCurrentUserController : CurrentUserController
CancellationToken cancellationToken,
[FromQuery(Name = "id")] HashSet<Guid> ids)
{
Attempt<IEnumerable<NodePermissions>, UserOperationStatus> permissionsAttempt = await _userService.GetDocumentPermissionsAsync(CurrentUserKey(_backOfficeSecurityAccessor), ids);
IUser currentUser = CurrentUser(_backOfficeSecurityAccessor);
NodePermissions[] permissions = (await _contentPermissionService.GetPermissionsAsync(currentUser, ids)).ToArray();
if (permissionsAttempt.Success is false)
// Preserve 404 behavior: if any requested ID was not found, return ContentNodeNotFound.
if (ids.Count > 0 && permissions.Length < ids.Count)
{
return UserOperationStatusResult(permissionsAttempt.Status);
return UserOperationStatusResult(UserOperationStatus.ContentNodeNotFound);
}
List<UserPermissionViewModel> viewModels = _mapper.MapEnumerable<NodePermissions, UserPermissionViewModel>(permissionsAttempt.Result);
List<UserPermissionViewModel> viewModels = _mapper.MapEnumerable<NodePermissions, UserPermissionViewModel>(permissions);
return Ok(new UserPermissionsResponseModel { Permissions = viewModels });
}
@@ -20,7 +20,6 @@ namespace Umbraco.Cms.Api.Management.Controllers.User.Current;
public class SetAvatarCurrentUserController : CurrentUserControllerBase
{
private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor;
private readonly IAuthorizationService _authorizationService;
private readonly IUserService _userService;
/// <summary>
@@ -29,13 +28,15 @@ public class SetAvatarCurrentUserController : CurrentUserControllerBase
/// <param name="backOfficeSecurityAccessor">Provides access to back office security features for the current user.</param>
/// <param name="authorizationService">Service used to authorize user actions.</param>
/// <param name="userService">Service for managing user-related operations.</param>
// TODO (V18): Remove the IAuthorizationService parameter from the constructor and the class, as it is not used in the current implementation.
public SetAvatarCurrentUserController(
IBackOfficeSecurityAccessor backOfficeSecurityAccessor,
#pragma warning disable IDE0060 // Remove unused parameter
IAuthorizationService authorizationService,
#pragma warning restore IDE0060 // Remove unused parameter
IUserService userService)
{
_backOfficeSecurityAccessor = backOfficeSecurityAccessor;
_authorizationService = authorizationService;
_userService = userService;
}
@@ -55,16 +56,6 @@ public class SetAvatarCurrentUserController : CurrentUserControllerBase
{
Guid userKey = CurrentUserKey(_backOfficeSecurityAccessor);
AuthorizationResult authorizationResult = await _authorizationService.AuthorizeResourceAsync(
User,
UserPermissionResource.WithKeys(userKey),
AuthorizationPolicies.UserPermissionByResource);
if (!authorizationResult.Succeeded)
{
return Forbidden();
}
UserOperationStatus result = await _userService.SetAvatarAsync(userKey, model.File.Id);
return result is UserOperationStatus.Success
@@ -0,0 +1,64 @@
using Asp.Versioning;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Api.Management.Factories;
using Umbraco.Cms.Api.Management.ViewModels.User;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.Membership;
using Umbraco.Cms.Core.Security;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Services.OperationStatus;
namespace Umbraco.Cms.Api.Management.Controllers.User.Current;
/// <summary>
/// Controller responsible for update information about the currently authenticated user.
/// </summary>
[ApiVersion("1.0")]
public class UpdateCurrentUserProfileController : CurrentUserControllerBase
{
private readonly IUserService _userService;
private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor;
private readonly IUserPresentationFactory _userPresentationFactory;
/// <summary>
/// Initializes a new instance of the <see cref="UpdateCurrentUserProfileController"/> class, which manages user update operations in the Umbraco backoffice API.
/// </summary>
/// <param name="userService">Service for managing user data and operations.</param>
/// <param name="userPresentationFactory">Factory for creating user presentation models.</param>
/// <param name="backOfficeSecurityAccessor">Accessor for back office security context.</param>
public UpdateCurrentUserProfileController(
IUserService userService,
IUserPresentationFactory userPresentationFactory,
IBackOfficeSecurityAccessor backOfficeSecurityAccessor)
{
_userService = userService;
_backOfficeSecurityAccessor = backOfficeSecurityAccessor;
_userPresentationFactory = userPresentationFactory;
}
/// <summary>
/// Updates the current user with new details provided in the request model.
/// </summary>
/// <param name="model">The request model containing updated current user information.</param>
/// <returns>An <see cref="IActionResult"/> indicating the outcome of the update operation.</returns>
[HttpPut("profile")]
[MapToApiVersion("1.0")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
[EndpointSummary("Updates current user profile.")]
[EndpointDescription("Updates current user profile with the details from the request model.")]
public async Task<IActionResult> UpdateCurrentUser(UpdateCurrentUserRequestModel model)
{
Guid userKey = CurrentUserKey(_backOfficeSecurityAccessor);
UserUpdateProfileModel updateModel = await _userPresentationFactory.CreateUpdateProfileModelAsync(model);
Attempt<IUser?, UserOperationStatus> result = await _userService.UpdateProfileAsync(userKey, updateModel);
return result.Success
? Ok()
: UserOperationStatusResult(result.Status);
}
}
@@ -63,6 +63,10 @@ public abstract class UserOrCurrentUserControllerBase : ManagementApiControllerB
.WithTitle("Cannot delete user")
.WithDetail("The user cannot be deleted.")
.Build()),
UserOperationStatus.CannotDeleteUserWithLoginHistory => BadRequest(problemDetailsBuilder
.WithTitle("Cannot delete user")
.WithDetail("This user has logged in and may be referenced by audit logs or content history. Disable the user instead of deleting them.")
.Build()),
UserOperationStatus.CannotDisableSelf => BadRequest(problemDetailsBuilder
.WithTitle("Cannot disable")
.WithDetail("A user cannot disable itself.")
@@ -1,6 +1,4 @@
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using OpenIddict.Server;
using Umbraco.Cms.Api.Common.DependencyInjection;
@@ -9,9 +7,9 @@ using Umbraco.Cms.Api.Management.Handlers;
using Umbraco.Cms.Api.Management.Middleware;
using Umbraco.Cms.Api.Management.Security;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Configuration.Models;
using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Core.Notifications;
using Umbraco.Cms.Core.Security;
using Umbraco.Cms.Infrastructure.Security;
using Umbraco.Cms.Web.Common.ApplicationBuilder;
@@ -27,12 +25,12 @@ public static class BackOfficeAuthBuilderExtensions
/// </summary>
/// <param name="builder">The <see cref="IUmbracoBuilder"/> to which back office authentication services will be added.</param>
/// <returns>The same <see cref="IUmbracoBuilder"/> instance with back office authentication configured.</returns>
[Obsolete("Use AddBackOffice() or AddBackOfficeSignIn() instead. Scheduled for removal in Umbraco 19.")]
public static IUmbracoBuilder AddBackOfficeAuthentication(this IUmbracoBuilder builder)
{
builder
.AddAuthentication()
.AddUmbracoOpenIddict()
.AddBackOfficeLogin();
.AddBackOfficeCookieAuthentication()
.AddBackOfficeOpenIddictServices();
return builder;
}
@@ -52,22 +50,13 @@ public static class BackOfficeAuthBuilderExtensions
return builder;
}
private static IUmbracoBuilder AddAuthentication(this IUmbracoBuilder builder)
/// <summary>
/// Registers backoffice cookie authentication schemes, cookie configuration, and authorization policies.
/// Does NOT register OpenIddict or the backoffice SPA infrastructure.
/// </summary>
internal static IUmbracoBuilder AddBackOfficeCookieAuthentication(this IUmbracoBuilder builder)
{
builder.Services.AddAuthentication();
builder.AddAuthorizationPolicies();
builder.Services.AddTransient<IBackOfficeApplicationManager, BackOfficeApplicationManager>();
builder.Services.AddSingleton<BackOfficeAuthorizationInitializationMiddleware>();
builder.Services.Configure<UmbracoPipelineOptions>(options => options.AddFilter(new BackofficePipelineFilter("Backoffice")));
return builder;
}
private static IUmbracoBuilder AddBackOfficeLogin(this IUmbracoBuilder builder)
{
builder.Services
.AddAuthentication()
builder.Services.AddAuthentication()
// Add our custom schemes which are cookie handlers
.AddCookie(Constants.Security.BackOfficeAuthenticationType)
@@ -93,7 +82,30 @@ public static class BackOfficeAuthBuilderExtensions
o.ExpireTimeSpan = TimeSpan.FromMinutes(5);
});
// Add OpnIddict server event handler to refresh the cookie that exposes the backoffice authentication outside the scope of the backoffice.
builder.Services.AddScoped<BackOfficeSecurityStampValidator>();
builder.Services.ConfigureOptions<ConfigureBackOfficeCookieOptions>();
builder.Services.ConfigureOptions<ConfigureBackOfficeExposedCookieOptions>();
builder.Services.ConfigureOptions<ConfigureBackOfficeSecurityStampValidatorOptions>();
builder.AddAuthorizationPolicies();
return builder;
}
/// <summary>
/// Registers OpenIddict services, the backoffice application manager, authorization initialization middleware,
/// and OpenIddict event handlers. These are only needed for the full backoffice SPA flow.
/// </summary>
internal static IUmbracoBuilder AddBackOfficeOpenIddictServices(this IUmbracoBuilder builder)
{
builder.AddUmbracoOpenIddict();
builder.Services.AddTransient<IBackOfficeApplicationManager, BackOfficeApplicationManager>();
builder.Services.AddScoped<IBackOfficeUserClientCredentialsManager, BackOfficeUserClientCredentialsManager>();
builder.Services.AddSingleton<BackOfficeAuthorizationInitializationMiddleware>();
builder.Services.Configure<UmbracoPipelineOptions>(options => options.AddFilter(new BackofficePipelineFilter("Backoffice")));
// Add OpenIddict server event handler to refresh the cookie that exposes the backoffice authentication outside the scope of the backoffice.
builder.Services.AddSingleton<ExposeBackOfficeAuthenticationOpenIddictServerEventsHandler>();
builder.Services.Configure<OpenIddictServerOptions>(options =>
{
@@ -109,11 +121,6 @@ public static class BackOfficeAuthBuilderExtensions
.Build());
});
builder.Services.AddScoped<BackOfficeSecurityStampValidator>();
builder.Services.ConfigureOptions<ConfigureBackOfficeCookieOptions>();
builder.Services.ConfigureOptions<ConfigureBackOfficeExposedCookieOptions>();
builder.Services.ConfigureOptions<ConfigureBackOfficeSecurityStampValidatorOptions>();
return builder;
}
}
@@ -1,6 +1,7 @@
using Microsoft.Extensions.DependencyInjection;
using Umbraco.Cms.Api.Management.Factories;
using Umbraco.Cms.Api.Management.Mapping.Document;
using Umbraco.Cms.Api.Management.Patchers;
using Umbraco.Cms.Api.Management.Services.PermissionFilter;
using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Core.Mapping;
@@ -21,6 +22,7 @@ internal static class DocumentBuilderExtensions
builder.Services.AddTransient<IDomainPresentationFactory, DomainPresentationFactory>();
builder.Services.AddTransient<IDocumentVersionPresentationFactory, DocumentVersionPresentationFactory>();
builder.Services.AddTransient<IDocumentCollectionPresentationFactory, DocumentCollectionPresentationFactory>();
builder.Services.AddTransient<IDocumentPatcher, DocumentPatcher>();
builder.WithCollectionBuilder<MapDefinitionCollectionBuilder>()
.Add<DocumentMapDefinition>()
@@ -5,14 +5,22 @@ using Umbraco.Cms.Api.Management.Services;
namespace Umbraco.Cms.Api.Management.DependencyInjection;
/// <summary>
/// Provides extension methods for configuring JSON serialization in the Umbraco CMS Management API.
/// Extension methods for registering JSON-related services.
/// </summary>
[Obsolete("JsonPatch.Net dependency and IJsonPatchService are being removed. Use the custom patch engine (IDocumentPatcher) instead. Scheduled for removal in Umbraco 19.")]
public static class JsonBuilderExtensions
{
/// <summary>
/// Adds JSON-related services to the Umbraco builder.
/// </summary>
/// <param name="builder">The Umbraco builder.</param>
/// <returns>The Umbraco builder.</returns>
internal static IUmbracoBuilder AddJson(this IUmbracoBuilder builder)
{
#pragma warning disable CS0618 // Type or member is obsolete
builder.Services
.AddTransient<IJsonPatchService, JsonPatchService>();
#pragma warning restore CS0618 // Type or member is obsolete
return builder;
}
@@ -1,6 +1,7 @@
using Microsoft.Extensions.DependencyInjection;
using Umbraco.Cms.Api.Management.Factories;
using Umbraco.Cms.Api.Management.Mapping.Member;
using Umbraco.Cms.Api.Management.Services;
using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Core.Mapping;
@@ -12,6 +13,8 @@ internal static class MemberBuilderExtensions
{
builder.Services.AddSingleton<IMemberPresentationFactory, MemberPresentationFactory>();
builder.Services.AddTransient<IMemberEditingPresentationFactory, MemberEditingPresentationFactory>();
builder.Services.AddTransient<IMemberPresentationService, MemberPresentationService>();
builder.Services.AddTransient<IMemberReferenceService, MemberReferenceService>();
builder.WithCollectionBuilder<MapDefinitionCollectionBuilder>().Add<MemberMapDefinition>();
@@ -11,6 +11,8 @@ internal static class TreeBuilderExtensions
internal static IUmbracoBuilder AddTrees(this IUmbracoBuilder builder)
{
builder.Services.AddTransient<IUserStartNodeEntitiesService, UserStartNodeEntitiesService>();
builder.Services.AddTransient<IDocumentStartNodeTreeFilterService, DocumentStartNodeTreeFilterService>();
builder.Services.AddTransient<IMediaStartNodeTreeFilterService, MediaStartNodeTreeFilterService>();
builder.Services.AddUnique<IPartialViewTreeService, PartialViewTreeService>();
builder.Services.AddUnique<IScriptTreeService, ScriptTreeService>();
@@ -17,20 +17,45 @@ public static partial class UmbracoBuilderExtensions
/// Adds all required components to run the Umbraco back office.
/// </summary>
/// <remarks>
/// This method calls <c>AddCore()</c> internally to register all core services,
/// then adds backoffice-specific services on top.
/// This method calls <c>AddCore()</c> and <see cref="AddBackOfficeSignIn"/> internally
/// to register core services, identity, and cookie authentication, then adds backoffice-specific
/// services on top (OpenIddict, backoffice SPA infrastructure, token management).
/// <para>
/// For frontend-only deployments that only need basic authentication with backoffice credentials
/// (no backoffice UI), use <see cref="AddBackOfficeSignIn"/> instead.
/// </para>
/// </remarks>
/// <param name="builder">The Umbraco builder.</param>
/// <param name="configureMvc">Optional action to configure the MVC builder.</param>
/// <returns>The Umbraco builder.</returns>
public static IUmbracoBuilder AddBackOffice(this IUmbracoBuilder builder, Action<IMvcBuilder>? configureMvc = null) =>
builder
.AddCore(configureMvc) // All core services
.AddBackOfficeCore() // Backoffice-specific: IBackOfficePathGenerator
.AddBackOfficeIdentity() // Backoffice user identity
.AddBackOfficeAuthentication() // OpenIddict, authorization policies
.AddTokenRevocation() // Token cleanup handlers
.AddMembersIdentity(); // Member identity (also needed for backoffice admin)
.AddCore(configureMvc) // All core services
.AddBackOfficeSignIn() // Identity + Cookie authentication
.AddBackOfficeCore() // IBackOfficePathGenerator, IBackOfficeEnabledMarker
.AddBackOfficeOpenIddictServices() // OpenIddict, application manager, middleware
.AddTokenRevocation() // Token cleanup handlers
.AddMembersIdentity(); // Member identity (also needed for backoffice admin)
/// <summary>
/// Adds backoffice identity and cookie authentication without the full backoffice UI or OpenIddict.
/// Use this for frontend-only deployments that need basic authentication with backoffice credentials.
/// </summary>
/// <remarks>
/// This registers the backoffice identity system (user manager, sign-in manager) and cookie authentication
/// schemes, but does NOT register OpenIddict, the Management API, or the backoffice SPA. It enables
/// <c>BasicAuthenticationMiddleware</c> to authenticate users via a standalone server-rendered login page.
/// <para>
/// Requires <c>AddCore()</c> to have been called first.
/// For full backoffice support, use <see cref="AddBackOffice"/> instead.
/// </para>
/// </remarks>
/// <param name="builder">The <see cref="IUmbracoBuilder"/> to configure.</param>
/// <returns>The same <see cref="IUmbracoBuilder"/> instance.</returns>
public static IUmbracoBuilder AddBackOfficeSignIn(this IUmbracoBuilder builder) =>
builder
.AddBackOfficeIdentity()
.AddBackOfficeCookieAuthentication();
/// <summary>
/// Registers the essential services required for the Umbraco back office, including the back office path generator and the physical file system implementation.
@@ -54,7 +54,8 @@ public static partial class UmbracoBuilderExtensions
factory.GetRequiredService<IUserRepository>(),
factory.GetRequiredService<IRuntimeState>(),
factory.GetRequiredService<IEventMessagesFactory>(),
factory.GetRequiredService<ILogger<BackOfficeUserStore>>()))
factory.GetRequiredService<ILogger<BackOfficeUserStore>>(),
factory.GetRequiredService<IBackOfficeUserReader>()))
.AddUserManager<IBackOfficeUserManager, BackOfficeUserManager>()
.AddSignInManager<IBackOfficeSignInManager, BackOfficeSignInManager>()
.AddClaimsPrincipalFactory<BackOfficeClaimsPrincipalFactory>()
@@ -67,7 +68,6 @@ public static partial class UmbracoBuilderExtensions
services.AddScoped<IInviteUriProvider, InviteUriProvider>();
services.AddScoped<IForgotPasswordUriProvider, ForgotPasswordUriProvider>();
services.AddScoped<IBackOfficePasswordChanger, BackOfficePasswordChanger>();
services.AddScoped<IBackOfficeUserClientCredentialsManager, BackOfficeUserClientCredentialsManager>();
services.AddSingleton<IBackOfficeUserPasswordChecker, NoopBackOfficeUserPasswordChecker>();
@@ -31,10 +31,14 @@ public static partial class UmbracoBuilderExtensions
builder.Services.AddUnique<IConflictingRouteService, ConflictingRouteService>();
builder.AddUmbracoApiOpenApiUI();
#pragma warning disable CS0618 // Type or member is obsolete
if (!services.Any(x => !x.IsKeyedService && x.ImplementationType == typeof(JsonPatchService)))
#pragma warning restore CS0618 // Type or member is obsolete
{
#pragma warning disable CS0618 // Type or member is obsolete
ModelsBuilderBuilderExtensions.AddModelsBuilder(builder)
.AddJson()
#pragma warning restore CS0618 // Type or member is obsolete
.AddInstaller()
.AddUpgrader()
.AddSearchManagement()
@@ -1,15 +1,41 @@
using Umbraco.Cms.Api.Management.Patching;
using Umbraco.Cms.Api.Management.ViewModels;
using Umbraco.Cms.Api.Management.ViewModels.Document;
using Umbraco.Cms.Api.Management.ViewModels.Patching;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.ContentEditing;
using Umbraco.Cms.Core.PropertyEditors;
using Umbraco.Cms.Core.Services;
using Umbraco.Extensions;
namespace Umbraco.Cms.Api.Management.Factories;
/// <summary>
/// Factory for creating and mapping presentation models used in document editing operations.
/// </summary>
internal sealed class DocumentEditingPresentationFactory : ContentEditingPresentationFactory<DocumentValueModel, DocumentVariantRequestModel>, IDocumentEditingPresentationFactory
{
private readonly PropertyEditorCollection _propertyEditorCollection;
private readonly IDataValueEditorFactory _dataValueEditorFactory;
private readonly ITemplateService _templateService;
/// <summary>
/// Maps a <see cref="CreateDocumentRequestModel"/> to a <see cref="ContentCreateModel"/>.
/// Initializes a new instance of the <see cref="DocumentEditingPresentationFactory"/> class.
/// </summary>
/// <param name="requestModel">The request model containing data to create the content.</param>
/// <returns>A <see cref="ContentCreateModel"/> representing the content to be created.</returns>
/// <param name="propertyEditorCollection">The collection of available property editors.</param>
/// <param name="dataValueEditorFactory">The factory for creating data value editors.</param>
/// <param name="templateService">The service for retrieving templates.</param>
public DocumentEditingPresentationFactory(
PropertyEditorCollection propertyEditorCollection,
IDataValueEditorFactory dataValueEditorFactory,
ITemplateService templateService)
{
_propertyEditorCollection = propertyEditorCollection;
_dataValueEditorFactory = dataValueEditorFactory;
_templateService = templateService;
}
/// <inheritdoc/>
public ContentCreateModel MapCreateModel(CreateDocumentRequestModel requestModel)
{
ContentCreateModel model = MapContentEditingModel<ContentCreateModel>(requestModel);
@@ -21,19 +47,46 @@ internal sealed class DocumentEditingPresentationFactory : ContentEditingPresent
return model;
}
/// <summary>
/// Maps the given <see cref="UpdateDocumentRequestModel"/> to a <see cref="ContentUpdateModel"/>.
/// </summary>
/// <param name="requestModel">The update document request model to map from.</param>
/// <returns>The mapped <see cref="ContentUpdateModel"/> instance.</returns>
/// <inheritdoc/>
public ContentUpdateModel MapUpdateModel(UpdateDocumentRequestModel requestModel)
=> MapUpdateContentModel<ContentUpdateModel>(requestModel);
/// <summary>
/// Maps a <see cref="ValidateUpdateDocumentRequestModel"/> to a <see cref="ValidateContentUpdateModel"/>, copying relevant validation data.
/// </summary>
/// <param name="requestModel">The request model containing the document update validation data.</param>
/// <returns>A <see cref="ValidateContentUpdateModel"/> populated with validation data from the request model.</returns>
/// <inheritdoc/>
public async Task<UpdateDocumentRequestModel> CreateUpdateRequestModelAsync(IContent content)
{
DocumentValueModel[] values = MapValuesToRequestModel(content.Properties);
DocumentVariantRequestModel[] variants = MapVariantsToRequestModel(content);
Guid? templateKey = content.TemplateId.HasValue
? (await _templateService.GetAsync(content.TemplateId.Value))?.Key
: null;
return new UpdateDocumentRequestModel
{
Values = values,
Variants = variants,
Template = templateKey.HasValue ? new ReferenceByIdModel { Id = templateKey.Value } : null,
};
}
/// <inheritdoc/>
public ContentPatchModel MapPatchModel(PatchDocumentRequestModel requestModel)
{
PatchOperationModel[] operations = requestModel.Operations.Select(op => new PatchOperationModel
{
Op = MapOperationType(op.Op),
Path = op.Path,
Value = op.Value,
}).ToArray();
return new ContentPatchModel
{
Operations = operations,
};
}
/// <inheritdoc/>
public ValidateContentUpdateModel MapValidateUpdateModel(ValidateUpdateDocumentRequestModel requestModel)
{
ValidateContentUpdateModel model = MapUpdateContentModel<ValidateContentUpdateModel>(requestModel);
@@ -42,6 +95,61 @@ internal sealed class DocumentEditingPresentationFactory : ContentEditingPresent
return model;
}
private DocumentValueModel[] MapValuesToRequestModel(IPropertyCollection properties)
{
Dictionary<string, IDataEditor> missingPropertyEditors = [];
return properties
.SelectMany(property => property
.Values
.Select(propertyValue =>
{
IDataEditor? propertyEditor = _propertyEditorCollection[property.PropertyType.PropertyEditorAlias];
if (propertyEditor is null && !missingPropertyEditors.TryGetValue(property.PropertyType.PropertyEditorAlias, out propertyEditor))
{
// Cache missing property editors to avoid creating multiple instances
propertyEditor = new MissingPropertyEditor(property.PropertyType.PropertyEditorAlias, _dataValueEditorFactory);
missingPropertyEditors[property.PropertyType.PropertyEditorAlias] = propertyEditor;
}
return new DocumentValueModel
{
Culture = propertyValue.Culture,
Segment = propertyValue.Segment,
Alias = property.Alias,
Value = propertyEditor.GetValueEditor().ToEditor(property, propertyValue.Culture, propertyValue.Segment),
};
}))
.WhereNotNull()
.ToArray();
}
private DocumentVariantRequestModel[] MapVariantsToRequestModel(IContent content)
{
IPropertyValue[] propertyValues = content.Properties.SelectMany(propertyCollection => propertyCollection.Values).ToArray();
var cultures = content.AvailableCultures.DefaultIfEmpty(null).ToArray();
// The default segment (null) must always be included
var segments = propertyValues.Select(property => property.Segment).Union([null]).Distinct().ToArray();
return cultures
.SelectMany(culture => segments.Select(segment => new DocumentVariantRequestModel
{
Culture = culture,
Segment = segment,
Name = content.GetCultureName(culture) ?? string.Empty,
}))
.ToArray();
}
private static PatchOperationType MapOperationType(string op) =>
op.ToLowerInvariant() switch
{
"replace" => PatchOperationType.Replace,
"add" => PatchOperationType.Add,
"remove" => PatchOperationType.Remove,
_ => throw new ArgumentException($"Unsupported operation type: {op}", nameof(op)),
};
private TUpdateModel MapUpdateContentModel<TUpdateModel>(UpdateDocumentRequestModel requestModel)
where TUpdateModel : ContentUpdateModel, new()
{
@@ -102,7 +102,7 @@ public class DocumentUrlFactory : IDocumentUrlFactory
if (await _previewService.TryEnterPreviewAsync(currentUser) is false)
{
_logger.LogError("A server error occured, could not initiate an authenticated preview state for the current user.");
_logger.LogError("A server error occurred, could not initiate an authenticated preview state for the current user.");
return null;
}
}
@@ -1,4 +1,6 @@
using Umbraco.Cms.Api.Management.ViewModels.Document;
using Umbraco.Cms.Api.Management.ViewModels.Document;
using Umbraco.Cms.Api.Management.ViewModels.Patching;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.ContentEditing;
namespace Umbraco.Cms.Api.Management.Factories;
@@ -16,16 +18,34 @@ public interface IDocumentEditingPresentationFactory
ContentCreateModel MapCreateModel(CreateDocumentRequestModel requestModel);
/// <summary>
/// Maps the given <see cref="Umbraco.Cms.Api.Management.Models.UpdateDocumentRequestModel" /> to a <see cref="Umbraco.Cms.Core.Models.ContentUpdateModel" />.
/// Maps the given <see cref="UpdateDocumentRequestModel" /> to a <see cref="ContentUpdateModel" />.
/// </summary>
/// <param name="requestModel">The update document request model to map from.</param>
/// <returns>The mapped content update model.</returns>
ContentUpdateModel MapUpdateModel(UpdateDocumentRequestModel requestModel);
// TODO (V19): Remove the default implementation.
/// <summary>
/// Maps the given <see cref="ValidateUpdateDocumentRequestModel"/> to a <see cref="ValidateContentUpdateModel"/> for validation purposes.
/// Creates an <see cref="UpdateDocumentRequestModel"/> from the given <see cref="IContent"/>,
/// mapping its properties, variants, and template into the request model representation.
/// </summary>
/// <param name="requestModel">The request model containing the data to validate.</param>
/// <returns>A <see cref="ValidateContentUpdateModel"/> representing the validated content update.</returns>
/// <param name="content">The content item to create the update request model from.</param>
/// <returns>An <see cref="UpdateDocumentRequestModel"/> representing the content.</returns>
Task<UpdateDocumentRequestModel> CreateUpdateRequestModelAsync(IContent content) => throw new NotImplementedException();
// TODO (V19): Remove the default implementation.
/// <summary>
/// Maps a <see cref="PatchDocumentRequestModel"/> to a <see cref="ContentPatchModel"/>,
/// extracting the affected cultures and segments from the patch operation paths.
/// </summary>
/// <param name="requestModel">The patch document request model.</param>
/// <returns>A <see cref="ContentPatchModel"/> containing the mapped operations and affected cultures/segments.</returns>
ContentPatchModel MapPatchModel(PatchDocumentRequestModel requestModel) => throw new NotImplementedException();
/// <summary>
/// Maps a <see cref="ValidateUpdateDocumentRequestModel"/> to a <see cref="ValidateContentUpdateModel"/> for update validation.
/// </summary>
/// <param name="requestModel">The validate update document request model.</param>
/// <returns>A <see cref="ValidateContentUpdateModel"/> ready for validation.</returns>
ValidateContentUpdateModel MapValidateUpdateModel(ValidateUpdateDocumentRequestModel requestModel);
}
@@ -3,6 +3,7 @@ using Umbraco.Cms.Api.Management.ViewModels.Member.Item;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.Entities;
using Umbraco.Cms.Core.Models.Membership;
using Umbraco.Cms.Core.Security;
namespace Umbraco.Cms.Api.Management.Factories;
@@ -40,4 +41,31 @@ public interface IMemberPresentationFactory
/// <param name="entity">The member entity to create the response model from.</param>
/// <returns>A MemberItemResponseModel representing the member entity.</returns>
MemberItemResponseModel CreateItemResponseModel(IMember entity);
/// <summary>
/// Creates a response model for an external-only member.
/// </summary>
/// <param name="member">The external member identity to create the response model from.</param>
/// <returns>A task that represents the asynchronous operation. The task result contains the <see cref="MemberResponseModel"/>.</returns>
// TODO (V19): Remove the default implementation.
Task<MemberResponseModel> CreateExternalMemberResponseModelAsync(ExternalMemberIdentity member)
=> Task.FromResult(new MemberResponseModel { Id = member.Key, Kind = MemberKind.ExternalOnly });
/// <summary>
/// Creates an item response model for an external-only member.
/// </summary>
/// <param name="member">The external member identity to create the item response model from.</param>
/// <returns>A <see cref="MemberItemResponseModel"/> representing the external member.</returns>
// TODO (V19): Remove the default implementation.
MemberItemResponseModel CreateExternalMemberItemResponseModel(ExternalMemberIdentity member)
=> new() { Id = member.Key, Kind = MemberKind.ExternalOnly };
/// <summary>
/// Creates a response model from a <see cref="MemberFilterItem"/> returned by the combined filter query.
/// </summary>
/// <param name="item">The filter item to create the response model from.</param>
/// <returns>A <see cref="MemberResponseModel"/> representing the filter item.</returns>
// TODO (V19): Remove the default implementation.
MemberResponseModel CreateFilterItemResponseModel(MemberFilterItem item)
=> new() { Id = item.Key, Kind = item.Kind };
}
@@ -31,6 +31,12 @@ public interface IUserPresentationFactory
/// </summary>
Task<UserUpdateModel> CreateUpdateModelAsync(Guid existingUserKey, UpdateUserRequestModel updateModel);
/// <summary>
/// Creates an update model for a current user based on the provided request model.
/// </summary>
// TODO V19: Remove default implementation
Task<UserUpdateProfileModel> CreateUpdateProfileModelAsync(UpdateCurrentUserRequestModel updateModel) => throw new NotImplementedException();
/// <summary>
/// Creates a response model for the current user based on the provided user.
/// </summary>
@@ -56,10 +62,10 @@ public interface IUserPresentationFactory
/// </summary>
UserItemResponseModel CreateItemResponseModel(IUser user);
/// <summary>
/// Asynchronously creates a response model containing the calculated start nodes for the specified user.
/// </summary>
/// <param name="user">The user for whom to calculate start nodes.</param>
/// <returns>A task representing the asynchronous operation. The task result contains the calculated user start nodes response model.</returns>
/// <summary>
/// Asynchronously creates a response model containing the calculated start nodes for the specified user.
/// </summary>
/// <param name="user">The user for whom to calculate start nodes.</param>
/// <returns>A task representing the asynchronous operation. The task result contains the calculated user start nodes response model.</returns>
Task<CalculatedUserStartNodesResponseModel> CreateCalculatedUserStartNodesResponseModelAsync(IUser user);
}
@@ -124,7 +124,7 @@ public class IndexPresentationFactory : IIndexPresentationFactory
}
catch (Exception e)
{
_logger.LogError(e, "An error occured trying to get the searcher name of index {IndexName}", index.Name);
_logger.LogError(e, "An error occurred trying to get the searcher name of index {IndexName}", index.Name);
name = "Could not determine searcher name because of error.";
return false;
}
@@ -139,7 +139,7 @@ public class IndexPresentationFactory : IIndexPresentationFactory
}
catch (Exception e)
{
_logger.LogError(e, "An error occured trying to get the document count of index {IndexName}", index.Name);
_logger.LogError(e, "An error occurred trying to get the document count of index {IndexName}", index.Name);
documentCount = 0;
return false;
}
@@ -154,7 +154,7 @@ public class IndexPresentationFactory : IIndexPresentationFactory
}
catch (Exception e)
{
_logger.LogError(e, "An error occured trying to get the field name count of index {IndexName}", index.Name);
_logger.LogError(e, "An error occurred trying to get the field name count of index {IndexName}", index.Name);
fieldNameCount = 0;
return false;
}
@@ -9,11 +9,13 @@ using Umbraco.Cms.Core.Mapping;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.Entities;
using Umbraco.Cms.Core.Models.Membership;
using Umbraco.Cms.Core.Security;
using Umbraco.Cms.Core.Services;
using Umbraco.Extensions;
namespace Umbraco.Cms.Api.Management.Factories;
/// <inheritdoc/>
internal sealed class MemberPresentationFactory : IMemberPresentationFactory
{
private readonly IUmbracoMapper _umbracoMapper;
@@ -22,6 +24,7 @@ internal sealed class MemberPresentationFactory : IMemberPresentationFactory
private readonly ITwoFactorLoginService _twoFactorLoginService;
private readonly IMemberGroupService _memberGroupService;
private readonly DeliveryApiSettings _deliveryApiSettings;
private readonly IExternalMemberService _externalMemberService;
private IEnumerable<Guid>? _clientCredentialsMemberKeys;
/// <summary>
@@ -33,13 +36,15 @@ internal sealed class MemberPresentationFactory : IMemberPresentationFactory
/// <param name="twoFactorLoginService">Service for handling two-factor authentication for members.</param>
/// <param name="memberGroupService">Service for managing member groups.</param>
/// <param name="deliveryApiSettings">The configuration options for the Delivery API.</param>
/// <param name="externalMemberService">Service for managing external-only members.</param>
public MemberPresentationFactory(
IUmbracoMapper umbracoMapper,
IMemberService memberService,
IMemberTypeService memberTypeService,
ITwoFactorLoginService twoFactorLoginService,
IMemberGroupService memberGroupService,
IOptions<DeliveryApiSettings> deliveryApiSettings)
IOptions<DeliveryApiSettings> deliveryApiSettings,
IExternalMemberService externalMemberService)
{
_umbracoMapper = umbracoMapper;
_memberService = memberService;
@@ -47,14 +52,10 @@ internal sealed class MemberPresentationFactory : IMemberPresentationFactory
_twoFactorLoginService = twoFactorLoginService;
_memberGroupService = memberGroupService;
_deliveryApiSettings = deliveryApiSettings.Value;
_externalMemberService = externalMemberService;
}
/// <summary>
/// Asynchronously creates a <see cref="MemberResponseModel"/> for the specified <see cref="IMember"/>, including or excluding sensitive data based on the current user's permissions.
/// </summary>
/// <param name="member">The member entity to map to a response model.</param>
/// <param name="currentUser">The user requesting the data, used to determine access to sensitive information.</param>
/// <returns>A task representing the asynchronous operation, with a <see cref="MemberResponseModel"/> as the result.</returns>
/// <inheritdoc/>
public async Task<MemberResponseModel> CreateResponseModelAsync(IMember member, IUser currentUser)
{
MemberResponseModel responseModel = _umbracoMapper.Map<MemberResponseModel>(member)!;
@@ -70,6 +71,7 @@ internal sealed class MemberPresentationFactory : IMemberPresentationFactory
: await RemoveSensitiveDataAsync(member, responseModel);
}
/// <inheritdoc/>
public async Task<IEnumerable<MemberResponseModel>> CreateMultipleAsync(IEnumerable<IMember> members, IUser currentUser)
{
var memberResponseModels = new List<MemberResponseModel>();
@@ -81,41 +83,101 @@ internal sealed class MemberPresentationFactory : IMemberPresentationFactory
return memberResponseModels;
}
/// <summary>
/// Creates a response model for a member item from the given entity.
/// </summary>
/// <param name="entity">The member entity to create the response model from.</param>
/// <returns>A <see cref="MemberItemResponseModel"/> representing the member.</returns>
/// <inheritdoc/>
public MemberItemResponseModel CreateItemResponseModel(IMemberEntitySlim entity)
=> CreateItemResponseModel<IMemberEntitySlim>(entity);
/// <summary>
/// Creates a response model for a member item based on the given member entity.
/// </summary>
/// <param name="entity">The member entity to create the response model from.</param>
/// <returns>A <see cref="MemberItemResponseModel"/> representing the member.</returns>
/// <inheritdoc/>
public MemberItemResponseModel CreateItemResponseModel(IMember entity)
=> CreateItemResponseModel<IMember>(entity);
/// <inheritdoc/>
public async Task<MemberResponseModel> CreateExternalMemberResponseModelAsync(ExternalMemberIdentity member)
{
IEnumerable<string> roles = await _externalMemberService.GetRolesAsync(member.Key);
IEnumerable<Guid> groupKeys = roles
.Select(x => _memberGroupService.GetByName(x))
.WhereNotNull()
.Select(x => x.Key)
.ToArray();
return new MemberResponseModel
{
Id = member.Key,
Email = member.Email,
Username = member.UserName,
IsApproved = member.IsApproved,
IsLockedOut = member.IsLockedOut,
IsTwoFactorEnabled = false,
FailedPasswordAttempts = 0,
LastLoginDate = member.LastLoginDate.HasValue ? new DateTimeOffset(member.LastLoginDate.Value, TimeSpan.Zero) : null,
LastLockoutDate = member.LastLockoutDate.HasValue ? new DateTimeOffset(member.LastLockoutDate.Value, TimeSpan.Zero) : null,
LastPasswordChangeDate = null,
Kind = MemberKind.ExternalOnly,
Variants = [new MemberVariantResponseModel
{
Name = member.Name ?? string.Empty,
CreateDate = new DateTimeOffset(member.CreateDate, TimeSpan.Zero),
UpdateDate = new DateTimeOffset(member.UpdateDate, TimeSpan.Zero),
}],
Values = Enumerable.Empty<MemberValueResponseModel>(),
MemberType = new MemberTypeReferenceResponseModel(),
Groups = groupKeys,
ProfileData = member.ProfileData,
};
}
/// <inheritdoc/>
public MemberItemResponseModel CreateExternalMemberItemResponseModel(ExternalMemberIdentity member) =>
new()
{
Id = member.Key,
MemberType = new MemberTypeReferenceResponseModel(),
Variants = [new VariantItemResponseModel { Name = member.Name ?? string.Empty, Culture = null }],
Kind = MemberKind.ExternalOnly,
};
/// <inheritdoc/>
public MemberResponseModel CreateFilterItemResponseModel(MemberFilterItem item) =>
new()
{
Id = item.Key,
Email = item.Email,
Username = item.UserName,
IsApproved = item.IsApproved,
IsLockedOut = item.IsLockedOut,
LastLoginDate = item.LastLoginDate.HasValue ? new DateTimeOffset(item.LastLoginDate.Value, TimeSpan.Zero) : null,
LastLockoutDate = item.LastLockoutDate.HasValue ? new DateTimeOffset(item.LastLockoutDate.Value, TimeSpan.Zero) : null,
LastPasswordChangeDate = item.LastPasswordChangeDate.HasValue ? new DateTimeOffset(item.LastPasswordChangeDate.Value, TimeSpan.Zero) : null,
Kind = item.Kind,
Variants = [new MemberVariantResponseModel { Name = item.Name ?? string.Empty }],
Values = [],
MemberType = new MemberTypeReferenceResponseModel
{
Id = item.MemberTypeKey ?? Guid.Empty,
Icon = item.MemberTypeIcon ?? string.Empty,
},
};
private MemberItemResponseModel CreateItemResponseModel<T>(T entity)
where T : ITreeEntity
=> new MemberItemResponseModel
=> new()
{
Id = entity.Key,
MemberType = _umbracoMapper.Map<MemberTypeReferenceResponseModel>(entity)!,
Variants = CreateVariantsItemResponseModels(entity),
Kind = GetMemberKind(entity.Key)
Kind = GetMemberKind(entity.Key),
};
private static IEnumerable<VariantItemResponseModel> CreateVariantsItemResponseModels(ITreeEntity entity)
=> new[]
{
=>
[
new VariantItemResponseModel
{
Name = entity.Name ?? string.Empty,
Culture = null
Culture = null,
}
};
];
private async Task<MemberResponseModel> RemoveSensitiveDataAsync(IMember member, MemberResponseModel responseModel)
{
@@ -1,12 +1,12 @@
using Microsoft.Extensions.DependencyInjection;
using Umbraco.Cms.Api.Management.ViewModels;
using Umbraco.Cms.Api.Management.ViewModels.Member.Item;
using Umbraco.Cms.Api.Management.ViewModels.MemberGroup.Item;
using Umbraco.Cms.Api.Management.ViewModels.PublicAccess;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Core.Mapping;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.Entities;
using Umbraco.Cms.Core.Security;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Services.OperationStatus;
using Umbraco.Cms.Web.Common.Security;
@@ -23,8 +23,10 @@ public class PublicAccessPresentationFactory : IPublicAccessPresentationFactory
private readonly IEntityService _entityService;
private readonly IMemberService _memberService;
private readonly IUmbracoMapper _mapper;
private readonly IMemberRoleManager _memberRoleManager;
private readonly IMemberPresentationFactory _memberPresentationFactory;
private readonly IMemberGroupService _memberGroupService;
// TODO (V19): When the obsolete constructor is removed, consider also remove the unused dependency on IMemberRoleManager.
/// <summary>
/// Initializes a new instance of the <see cref="PublicAccessPresentationFactory"/> class.
@@ -34,18 +36,37 @@ public class PublicAccessPresentationFactory : IPublicAccessPresentationFactory
/// <param name="mapper">The Umbraco mapper for mapping entities to response models.</param>
/// <param name="memberRoleManager">The member role manager for resolving member groups.</param>
/// <param name="memberPresentationFactory">The member presentation factory for creating member item response models.</param>
/// <param name="memberGroupService">The member group service for resolving member groups by name.</param>
public PublicAccessPresentationFactory(
IEntityService entityService,
IMemberService memberService,
IUmbracoMapper mapper,
IMemberRoleManager memberRoleManager,
IMemberPresentationFactory memberPresentationFactory,
IMemberGroupService memberGroupService)
{
_entityService = entityService;
_memberService = memberService;
_mapper = mapper;
_memberPresentationFactory = memberPresentationFactory;
_memberGroupService = memberGroupService;
}
[Obsolete("Please use the constructor with all parameters. Scheduled for removal in Umbraco 19.")]
public PublicAccessPresentationFactory(
IEntityService entityService,
IMemberService memberService,
IUmbracoMapper mapper,
IMemberRoleManager memberRoleManager,
IMemberPresentationFactory memberPresentationFactory)
: this(
entityService,
memberService,
mapper,
memberRoleManager,
memberPresentationFactory,
StaticServiceProvider.Instance.GetRequiredService<IMemberGroupService>())
{
_entityService = entityService;
_memberService = memberService;
_mapper = mapper;
_memberRoleManager = memberRoleManager;
_memberPresentationFactory = memberPresentationFactory;
}
/// <inheritdoc/>
@@ -107,21 +128,15 @@ public class PublicAccessPresentationFactory : IPublicAccessPresentationFactory
.Select(_memberPresentationFactory.CreateItemResponseModel)
.ToArray();
var allGroups = _memberRoleManager.Roles.Where(x => x.Name != null).ToDictionary(x => x.Name!);
IEnumerable<UmbracoIdentityRole> identityRoles = entry.Rules
// Resolve groups via IMemberGroupService so custom implementations (e.g. backed by an external
// user store) are honoured here, rather than going directly to IMemberRoleManager/IEntityService.
MemberGroupItemResponseModel[] memberGroups = entry.Rules
.Where(rule => rule.RuleType == Constants.Conventions.PublicAccess.MemberRoleRuleType)
.Select(rule =>
rule.RuleValue is not null && allGroups.TryGetValue(rule.RuleValue, out UmbracoIdentityRole? memberRole)
? memberRole
: null)
.Select(rule => rule.RuleValue is null ? null : _memberGroupService.GetByName(rule.RuleValue))
.WhereNotNull()
.Select(group => _mapper.Map<MemberGroupItemResponseModel>(group)!)
.ToArray();
IEnumerable<IEntitySlim> groupsEntities = identityRoles.Any()
? _entityService.GetAll(UmbracoObjectTypes.MemberGroup, identityRoles.Select(x => Convert.ToInt32(x.Id)).ToArray())
: Enumerable.Empty<IEntitySlim>();
MemberGroupItemResponseModel[] memberGroups = groupsEntities.Select(x => _mapper.Map<MemberGroupItemResponseModel>(x)!).ToArray();
var responseModel = new PublicAccessResponseModel
{
Members = members,
@@ -1,5 +1,6 @@
using Umbraco.Cms.Api.Management.ViewModels;
using Umbraco.Cms.Api.Management.ViewModels.RedirectUrlManagement;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Routing;
@@ -33,12 +34,12 @@ public class RedirectUrlPresentationFactory : IRedirectUrlPresentationFactory
{
var destinationUrl = source.ContentId > 0
? _publishedUrlProvider.GetUrl(source.ContentId, culture: source.Culture)
: "#";
: Constants.Routing.Unroutable;
var originalUrl = _publishedUrlProvider.GetUrlFromRoute(source.ContentId, source.Url, source.Culture);
// Even if the URL could not be extracted from the route, if we have a path as a the route for the original URL, we should display it.
if (originalUrl == "#" && source.Url.StartsWith('/'))
if (originalUrl == Constants.Routing.Unroutable && source.Url.StartsWith('/'))
{
originalUrl = source.Url;
}
@@ -39,6 +39,7 @@ public class UserPresentationFactory : IUserPresentationFactory
private readonly IBackOfficeExternalLoginProviders _externalLoginProviders;
private readonly SecuritySettings _securitySettings;
private readonly Dictionary<Type, IPermissionPresentationMapper> _permissionPresentationMappersByType;
private readonly IContentPermissionService _contentPermissionService;
/// <summary>
/// Initializes a new instance of the <see cref="UserPresentationFactory"/> class.
@@ -54,6 +55,7 @@ public class UserPresentationFactory : IUserPresentationFactory
/// <param name="securitySettings">Provides access to security-related configuration settings.</param>
/// <param name="externalLoginProviders">Manages back office external login providers.</param>
/// <param name="permissionPresentationMappers">Collection of mappers for permission presentation models.</param>
/// <param name="contentPermissionService">Service for managing content permissions.</param>
public UserPresentationFactory(
IEntityService entityService,
AppCaches appCaches,
@@ -65,7 +67,8 @@ public class UserPresentationFactory : IUserPresentationFactory
IPasswordConfigurationPresentationFactory passwordConfigurationPresentationFactory,
IOptionsSnapshot<SecuritySettings> securitySettings,
IBackOfficeExternalLoginProviders externalLoginProviders,
IEnumerable<IPermissionPresentationMapper> permissionPresentationMappers)
IEnumerable<IPermissionPresentationMapper> permissionPresentationMappers,
IContentPermissionService contentPermissionService)
{
_entityService = entityService;
_appCaches = appCaches;
@@ -78,6 +81,50 @@ public class UserPresentationFactory : IUserPresentationFactory
_securitySettings = securitySettings.Value;
_absoluteUrlBuilder = absoluteUrlBuilder;
_permissionPresentationMappersByType = permissionPresentationMappers.ToDictionary(x => x.PresentationModelToHandle);
_contentPermissionService = contentPermissionService;
}
/// <summary>
/// Initializes a new instance of the <see cref="UserPresentationFactory"/> class.
/// </summary>
/// <param name="entityService">Service for accessing and managing entities.</param>
/// <param name="appCaches">Provides application-level caching functionality.</param>
/// <param name="mediaFileManager">Manages media file storage and retrieval.</param>
/// <param name="imageUrlGenerator">Generates URLs for images.</param>
/// <param name="userGroupPresentationFactory">Factory for creating user group presentation models.</param>
/// <param name="absoluteUrlBuilder">Builds absolute URLs for resources.</param>
/// <param name="emailSender">Handles sending emails.</param>
/// <param name="passwordConfigurationPresentationFactory">Factory for password configuration presentation models.</param>
/// <param name="securitySettings">Provides access to security-related configuration settings.</param>
/// <param name="externalLoginProviders">Manages back office external login providers.</param>
/// <param name="permissionPresentationMappers">Collection of mappers for permission presentation models.</param>
[Obsolete("Please use the constructor with all parameters. Scheduled for removal in Umbraco 19.")]
public UserPresentationFactory(
IEntityService entityService,
AppCaches appCaches,
MediaFileManager mediaFileManager,
IImageUrlGenerator imageUrlGenerator,
IUserGroupPresentationFactory userGroupPresentationFactory,
IAbsoluteUrlBuilder absoluteUrlBuilder,
IEmailSender emailSender,
IPasswordConfigurationPresentationFactory passwordConfigurationPresentationFactory,
IOptionsSnapshot<SecuritySettings> securitySettings,
IBackOfficeExternalLoginProviders externalLoginProviders,
IEnumerable<IPermissionPresentationMapper> permissionPresentationMappers)
: this(
entityService,
appCaches,
mediaFileManager,
imageUrlGenerator,
userGroupPresentationFactory,
absoluteUrlBuilder,
emailSender,
passwordConfigurationPresentationFactory,
securitySettings,
externalLoginProviders,
permissionPresentationMappers,
StaticServiceProvider.Instance.GetRequiredService<IContentPermissionService>())
{
}
/// <inheritdoc/>
@@ -227,7 +274,9 @@ public class UserPresentationFactory : IUserPresentationFactory
ISet<ReferenceByIdModel> documentStartNodeKeys = GetKeysFromIds(contentStartNodeIds, UmbracoObjectTypes.Document);
HashSet<IPermissionPresentationModel> permissions = GetAggregatedGranularPermissions(user, presentationGroups);
var fallbackPermissions = presentationGroups.SelectMany(x => x.FallbackPermissions).ToHashSet();
ISet<string> fallbackPermissions = await _contentPermissionService.FilterFallbackPermissionsAsync(
user,
presentationGroups.SelectMany(x => x.FallbackPermissions).ToHashSet());
var hasAccessToAllLanguages = presentationGroups.Any(x => x.HasAccessToAllLanguages);
@@ -340,4 +389,15 @@ public class UserPresentationFactory : IUserPresentationFactory
private static bool HasRootAccess(IEnumerable<int>? startNodeIds)
=> startNodeIds?.Contains(Constants.System.Root) is true;
/// <inheritdoc/>
public Task<UserUpdateProfileModel> CreateUpdateProfileModelAsync(UpdateCurrentUserRequestModel updateModel)
{
var model = new UserUpdateProfileModel
{
LanguageIsoCode = updateModel.LanguageIsoCode
};
return Task.FromResult(model);
}
}
@@ -41,6 +41,7 @@ public class ItemTypeMapDefinition : IMapDefinition
mapper.Define<IMediaType, MediaTypeItemResponseModel>((_, _) => new MediaTypeItemResponseModel(), Map);
mapper.Define<MediaTypeFileExtensionMatchResult, AllowedMediaTypeItemResponseModel>((_, _) => new AllowedMediaTypeItemResponseModel(), Map);
mapper.Define<IEntitySlim, MemberGroupItemResponseModel>((_, _) => new MemberGroupItemResponseModel(), Map);
mapper.Define<IMemberGroup, MemberGroupItemResponseModel>((_, _) => new MemberGroupItemResponseModel(), Map);
mapper.Define<ITemplate, TemplateItemResponseModel>((_, _) => new TemplateItemResponseModel { Alias = string.Empty }, Map);
mapper.Define<IMemberType, MemberTypeItemResponseModel>((_, _) => new MemberTypeItemResponseModel(), Map);
mapper.Define<IRelationType, RelationTypeItemResponseModel>((_, _) => new RelationTypeItemResponseModel(), Map);
@@ -105,6 +106,13 @@ public class ItemTypeMapDefinition : IMapDefinition
target.Id = source.Key;
}
// Umbraco.Code.MapAll -Flags
private static void Map(IMemberGroup source, MemberGroupItemResponseModel target, MapperContext context)
{
target.Name = source.Name ?? string.Empty;
target.Id = source.Key;
}
// Umbraco.Code.MapAll -Flags
private static void Map(ITemplate source, TemplateItemResponseModel target, MapperContext context)
{
@@ -48,7 +48,7 @@ public class MemberMapDefinition : ContentMapDefinition<IMember, MemberValueResp
public void DefineMaps(IUmbracoMapper mapper)
=> mapper.Define<IMember, MemberResponseModel>((_, _) => new MemberResponseModel(), Map);
// Umbraco.Code.MapAll -IsTwoFactorEnabled -Groups -Kind -Flags
// Umbraco.Code.MapAll -IsTwoFactorEnabled -Groups -Kind -Flags -ProfileData
private void Map(IMember source, MemberResponseModel target, MapperContext context)
{
target.Id = source.Key;
@@ -19,16 +19,30 @@ namespace Umbraco.Cms.Api.Management.Mapping.Permissions;
/// </remarks>
public class DocumentPermissionMapper : IPermissionPresentationMapper, IPermissionMapper
{
private readonly Lazy<IEntityService> _entityService;
private readonly Lazy<IUserService> _userService;
private readonly Lazy<IContentPermissionService> _contentPermissionService;
/// <summary>
/// Initializes a new instance of the <see cref="DocumentPermissionMapper"/> class.
/// </summary>
/// <param name="entityService">The entity service.</param>
/// <param name="userService">The user service.</param>
/// <param name="contentPermissionService">The content permission service.</param>
// TODO (V19): Remove the entityService and userService parameters as they are not used in the current implementation.
public DocumentPermissionMapper(
Lazy<IEntityService> entityService,
Lazy<IUserService> userService,
Lazy<IContentPermissionService> contentPermissionService) => _contentPermissionService = contentPermissionService;
/// <summary>
/// Initializes a new instance of the <see cref="DocumentPermissionMapper"/> class.
/// </summary>
[Obsolete("Please use the constructor with all parameters. Scheduled for removal in Umbraco 19.")]
public DocumentPermissionMapper(Lazy<IEntityService> entityService, Lazy<IUserService> userService)
: this(
entityService,
userService,
new Lazy<IContentPermissionService>(StaticServiceProvider.Instance.GetRequiredService<IContentPermissionService>))
{
_entityService = entityService;
_userService = userService;
}
/// <inheritdoc/>
@@ -110,25 +124,18 @@ public class DocumentPermissionMapper : IPermissionPresentationMapper, IPermissi
.Distinct()
.ToArray();
// Batch retrieve all documents by their keys.
var documents = _entityService.Value.GetAll<IContent>(documentKeysWithGranularPermissions)
.ToDictionary(doc => doc.Key, doc => doc.Path);
// Resolve permissions through IContentPermissionService so custom implementations are respected.
IEnumerable<NodePermissions> permissions = _contentPermissionService.Value
.GetPermissionsAsync(user, documentKeysWithGranularPermissions)
.GetAwaiter()
.GetResult();
// Iterate through each document key that has granular permissions.
foreach (Guid documentKey in documentKeysWithGranularPermissions)
foreach (NodePermissions nodePermission in permissions)
{
// Retrieve the path from the pre-fetched documents.
if (!documents.TryGetValue(documentKey, out var path) || string.IsNullOrEmpty(path))
{
continue;
}
// With the path we can call the same logic as used server-side for authorizing access to resources.
EntityPermissionSet permissionsForPath = _userService.Value.GetPermissionsForPath(user, path);
yield return new DocumentPermissionPresentationModel
{
Document = new ReferenceByIdModel(documentKey),
Verbs = permissionsForPath.GetAllPermissions(),
Document = new ReferenceByIdModel(nodePermission.NodeKey),
Verbs = nodePermission.Permissions,
};
}
}
@@ -18,7 +18,7 @@ namespace Umbraco.Cms.Api.Management.Middleware;
public class BackOfficeAuthorizationInitializationMiddleware : IMiddleware
{
private SemaphoreSlim _firstBackOfficeRequestLocker = new(1); // this only works because this is a singleton
private ISet<string> _knownHosts = new HashSet<string>(); // this only works because this is a singleton
private ISet<string> _knownHosts = new HashSet<string>(StringComparer.OrdinalIgnoreCase); // this only works because this is a singleton
private readonly UmbracoRequestPaths _umbracoRequestPaths;
private readonly IServiceProvider _serviceProvider;
@@ -79,30 +79,48 @@ public class BackOfficeAuthorizationInitializationMiddleware : IMiddleware
await _firstBackOfficeRequestLocker.WaitAsync();
// NOTE: _knownHosts is not thread safe; check again after entering the semaphore
if (_knownHosts.Add(host) is false)
try
{
// NOTE: _knownHosts is not thread safe; check again after entering the semaphore.
if (_knownHosts.Contains(host))
{
return;
}
// Ensure we explicitly add UmbracoApplicationUrl if configured (https://github.com/umbraco/Umbraco-CMS/issues/16179).
var hostsToRegister = new HashSet<string>(StringComparer.OrdinalIgnoreCase) { host };
if (_webRoutingSettings.UmbracoApplicationUrl.IsNullOrWhiteSpace() is false)
{
hostsToRegister.Add(_webRoutingSettings.UmbracoApplicationUrl);
}
// Merge with already-known hosts so the OpenIddict application includes all redirect URIs.
foreach (var knownHost in _knownHosts)
{
hostsToRegister.Add(knownHost);
}
Uri[] backOfficeHosts = hostsToRegister
.Select(h => Uri.TryCreate(h, UriKind.Absolute, out Uri? hostUri)
? hostUri
: null)
.WhereNotNull()
.ToArray();
using IServiceScope scope = _serviceProvider.CreateScope();
IBackOfficeApplicationManager backOfficeApplicationManager = scope.ServiceProvider.GetRequiredService<IBackOfficeApplicationManager>();
await backOfficeApplicationManager.EnsureBackOfficeApplicationAsync(backOfficeHosts);
// Only mark hosts as known after successful registration, so a transient failure
// (e.g. database contention during an unattended upgrade) is retried on the next request.
foreach (var registered in hostsToRegister)
{
_knownHosts.Add(registered);
}
}
finally
{
_firstBackOfficeRequestLocker.Release();
return;
}
// ensure we explicitly add UmbracoApplicationUrl if configured (https://github.com/umbraco/Umbraco-CMS/issues/16179)
if (_webRoutingSettings.UmbracoApplicationUrl.IsNullOrWhiteSpace() is false)
{
_knownHosts.Add(_webRoutingSettings.UmbracoApplicationUrl);
}
Uri[] backOfficeHosts = _knownHosts
.Select(host => Uri.TryCreate(host, UriKind.Absolute, out Uri? hostUri)
? hostUri
: null)
.WhereNotNull()
.ToArray();
using IServiceScope scope = _serviceProvider.CreateScope();
IBackOfficeApplicationManager backOfficeApplicationManager = scope.ServiceProvider.GetRequiredService<IBackOfficeApplicationManager>();
await backOfficeApplicationManager.EnsureBackOfficeApplicationAsync(backOfficeHosts);
_firstBackOfficeRequestLocker.Release();
}
}
+439 -10
View File
@@ -9632,6 +9632,157 @@
]
}
},
"/umbraco/management/api/v1/document/{id}/patch": {
"patch": {
"tags": [
"Document"
],
"summary": "Make partial updates to a document. For more information, see the documentation at https://docs.umbraco.com/umbraco-cms/reference/management-api/patching/document-endpoint-guide or https://docs.umbraco.com/umbraco-cms/reference/management-api/patching/document-endpoint-spec",
"operationId": "PatchDocumentByIdPatch",
"parameters": [
{
"name": "id",
"in": "path",
"required": true,
"schema": {
"type": "string",
"format": "uuid"
}
}
],
"requestBody": {
"content": {
"application/json-patch+json": {
"schema": {
"oneOf": [
{
"$ref": "#/components/schemas/PatchDocumentRequestModel"
}
]
}
}
}
},
"responses": {
"200": {
"description": "OK",
"headers": {
"Umb-Notifications": {
"description": "The list of notifications produced during the request.",
"schema": {
"type": "array",
"items": {
"$ref": "#/components/schemas/NotificationHeaderModel"
},
"nullable": true
}
}
}
},
"400": {
"description": "Bad Request",
"headers": {
"Umb-Notifications": {
"description": "The list of notifications produced during the request.",
"schema": {
"type": "array",
"items": {
"$ref": "#/components/schemas/NotificationHeaderModel"
},
"nullable": true
}
}
},
"content": {
"application/json": {
"schema": {
"oneOf": [
{
"$ref": "#/components/schemas/ProblemDetails"
}
]
}
}
}
},
"404": {
"description": "Not Found",
"headers": {
"Umb-Notifications": {
"description": "The list of notifications produced during the request.",
"schema": {
"type": "array",
"items": {
"$ref": "#/components/schemas/NotificationHeaderModel"
},
"nullable": true
}
}
},
"content": {
"application/json": {
"schema": {
"oneOf": [
{
"$ref": "#/components/schemas/ProblemDetails"
}
]
}
}
}
},
"422": {
"description": "Unprocessable Content",
"headers": {
"Umb-Notifications": {
"description": "The list of notifications produced during the request.",
"schema": {
"type": "array",
"items": {
"$ref": "#/components/schemas/NotificationHeaderModel"
},
"nullable": true
}
}
},
"content": {
"application/json": {
"schema": {
"oneOf": [
{
"$ref": "#/components/schemas/ProblemDetails"
}
]
}
}
}
},
"401": {
"description": "The resource is protected and requires an authentication token"
},
"403": {
"description": "The authenticated user does not have access to this resource",
"headers": {
"Umb-Notifications": {
"description": "The list of notifications produced during the request.",
"schema": {
"type": "array",
"items": {
"$ref": "#/components/schemas/NotificationHeaderModel"
},
"nullable": true
}
}
}
}
},
"security": [
{
"Backoffice-User": [ ]
}
]
}
},
"/umbraco/management/api/v1/document/{id}/preview-url": {
"get": {
"tags": [
@@ -36848,6 +36999,91 @@
}
},
"/umbraco/management/api/v1/user/current/avatar": {
"delete": {
"tags": [
"User"
],
"summary": "Clears the current user's avatar.",
"description": "Removes the avatar image for the currently authenticated user.",
"operationId": "DeleteUserCurrentAvatar",
"responses": {
"200": {
"description": "OK",
"headers": {
"Umb-Notifications": {
"description": "The list of notifications produced during the request.",
"schema": {
"type": "array",
"items": {
"$ref": "#/components/schemas/NotificationHeaderModel"
},
"nullable": true
}
}
}
},
"400": {
"description": "Bad Request",
"headers": {
"Umb-Notifications": {
"description": "The list of notifications produced during the request.",
"schema": {
"type": "array",
"items": {
"$ref": "#/components/schemas/NotificationHeaderModel"
},
"nullable": true
}
}
},
"content": {
"application/json": {
"schema": {
"oneOf": [
{
"$ref": "#/components/schemas/ProblemDetails"
}
]
}
}
}
},
"404": {
"description": "Not Found",
"headers": {
"Umb-Notifications": {
"description": "The list of notifications produced during the request.",
"schema": {
"type": "array",
"items": {
"$ref": "#/components/schemas/NotificationHeaderModel"
},
"nullable": true
}
}
},
"content": {
"application/json": {
"schema": {
"oneOf": [
{
"$ref": "#/components/schemas/ProblemDetails"
}
]
}
}
}
},
"401": {
"description": "The resource is protected and requires an authentication token"
}
},
"security": [
{
"Backoffice-User": [ ]
}
]
},
"post": {
"tags": [
"User"
@@ -37195,14 +37431,11 @@
"content": {
"application/json": {
"schema": {
"type": "array",
"items": {
"oneOf": [
{
"$ref": "#/components/schemas/UserPermissionsResponseModel"
}
]
}
"oneOf": [
{
"$ref": "#/components/schemas/UserPermissionsResponseModel"
}
]
}
}
}
@@ -37294,6 +37527,124 @@
]
}
},
"/umbraco/management/api/v1/user/current/profile": {
"put": {
"tags": [
"User"
],
"summary": "Updates current user profile.",
"description": "Updates current user profile with the details from the request model.",
"operationId": "PutUserCurrentProfile",
"requestBody": {
"content": {
"application/json": {
"schema": {
"oneOf": [
{
"$ref": "#/components/schemas/UpdateCurrentUserRequestModel"
}
]
}
},
"text/json": {
"schema": {
"oneOf": [
{
"$ref": "#/components/schemas/UpdateCurrentUserRequestModel"
}
]
}
},
"application/*+json": {
"schema": {
"oneOf": [
{
"$ref": "#/components/schemas/UpdateCurrentUserRequestModel"
}
]
}
}
}
},
"responses": {
"200": {
"description": "OK",
"headers": {
"Umb-Notifications": {
"description": "The list of notifications produced during the request.",
"schema": {
"type": "array",
"items": {
"$ref": "#/components/schemas/NotificationHeaderModel"
},
"nullable": true
}
}
}
},
"400": {
"description": "Bad Request",
"headers": {
"Umb-Notifications": {
"description": "The list of notifications produced during the request.",
"schema": {
"type": "array",
"items": {
"$ref": "#/components/schemas/NotificationHeaderModel"
},
"nullable": true
}
}
},
"content": {
"application/json": {
"schema": {
"oneOf": [
{
"$ref": "#/components/schemas/ProblemDetails"
}
]
}
}
}
},
"404": {
"description": "Not Found",
"headers": {
"Umb-Notifications": {
"description": "The list of notifications produced during the request.",
"schema": {
"type": "array",
"items": {
"$ref": "#/components/schemas/NotificationHeaderModel"
},
"nullable": true
}
}
},
"content": {
"application/json": {
"schema": {
"oneOf": [
{
"$ref": "#/components/schemas/ProblemDetails"
}
]
}
}
}
},
"401": {
"description": "The resource is protected and requires an authentication token"
}
},
"security": [
{
"Backoffice-User": [ ]
}
]
}
},
"/umbraco/management/api/v1/user/disable": {
"post": {
"tags": [
@@ -45917,7 +46268,8 @@
"MemberKindModel": {
"enum": [
"Default",
"Api"
"Api",
"ExternalOnly"
],
"type": "string"
},
@@ -46058,6 +46410,10 @@
},
"kind": {
"$ref": "#/components/schemas/MemberKindModel"
},
"profileData": {
"type": "string",
"nullable": true
}
},
"additionalProperties": false
@@ -48761,6 +49117,47 @@
},
"additionalProperties": false
},
"PatchDocumentRequestModel": {
"required": [
"operations"
],
"type": "object",
"properties": {
"operations": {
"minItems": 1,
"type": "array",
"items": {
"oneOf": [
{
"$ref": "#/components/schemas/PatchOperationRequestModel"
}
]
}
}
},
"additionalProperties": false
},
"PatchOperationRequestModel": {
"required": [
"op",
"path"
],
"type": "object",
"properties": {
"op": {
"minLength": 1,
"type": "string"
},
"path": {
"minLength": 1,
"type": "string"
},
"value": {
"nullable": true
}
},
"additionalProperties": false
},
"ProblemDetails": {
"type": "object",
"properties": {
@@ -49652,6 +50049,7 @@
"required": [
"allowLocalLogin",
"allowPasswordReset",
"signalR",
"umbracoCssPath",
"versionCheckPeriod"
],
@@ -49669,6 +50067,13 @@
},
"umbracoCssPath": {
"type": "string"
},
"signalR": {
"oneOf": [
{
"$ref": "#/components/schemas/SignalRClientSettingsResponseModel"
}
]
}
},
"additionalProperties": false
@@ -49744,6 +50149,18 @@
},
"additionalProperties": false
},
"SignalRClientSettingsResponseModel": {
"required": [
"skipNegotiation"
],
"type": "object",
"properties": {
"skipNegotiation": {
"type": "boolean"
}
},
"additionalProperties": false
},
"SortingRequestModel": {
"required": [
"sorting"
@@ -50759,6 +51176,18 @@
},
"additionalProperties": false
},
"UpdateCurrentUserRequestModel": {
"required": [
"languageIsoCode"
],
"type": "object",
"properties": {
"languageIsoCode": {
"type": "string"
}
},
"additionalProperties": false
},
"UpdateDataTypeRequestModel": {
"required": [
"editorAlias",
@@ -53245,4 +53674,4 @@
"name": "Webhook"
}
]
}
}
@@ -0,0 +1,23 @@
namespace Umbraco.Cms.Api.Management.OperationStatus;
/// <summary>
/// Operation status for PATCH operations at the API layer.
/// This is distinct from ContentEditingOperationStatus which is for service layer operations.
/// </summary>
public enum ContentPatchingOperationStatus
{
/// <summary>
/// The operation was successful.
/// </summary>
Success,
/// <summary>
/// One or more PATCH operations were invalid (invalid path syntax, unsupported operation type, missing required value).
/// </summary>
InvalidOperation,
/// <summary>
/// The target document could not be found.
/// </summary>
NotFound,
}

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