Compare commits

...
Author SHA1 Message Date
Laura Neto 09acba310f Merge branch 'v18/feature/delivery-api-document-type-schema-generation-clean' into v18/task/delivery-api-openapi-sample-content 2026-05-06 11:16:44 +02:00
Laura Neto 93a13648b7 Qualify properties model schema IDs by item type
Document, element, and media types share the same alias namespace
across content/media (a doc-type and a media-type can use the same
alias), so a "{Schema}PropertiesModel" naming scheme could collide.

Properties model schemas now follow the same Content/Element/Media
suffix as their parent *Model schema:

- Document type: ArticlePageContentPropertiesModel
- Element type:  TestElementElementPropertiesModel
- Media type:    VideoMediaPropertiesModel

Composition references look up each composition's own IsElement so
that a doc-type composing an element-type (allowed in the UI) still
references the correct ElementPropertiesModel schema.
2026-05-06 11:15:46 +02:00
Laura Neto 417eae32a7 Preserve casing of content type aliases in OpenAPI schema IDs
Replaces the legacy ModelsBuilder-style ToCleanString tokenizer with
ToFirstUpperInvariant. The tokenizer split aliases on case boundaries
and mangled capital-letter runs (e.g. "xMLSitemap" -> "XMlsitemap"),
making the typed schema names harder to read for OpenAPI consumers.
Since content type aliases are already valid identifiers, only the
first character needs uppercasing.

Also adds an "xMLSitemap" sample type to the integration tests to
cover the casing-preservation behavior.
2026-05-06 10:48:10 +02:00
Laura Neto c45813bebd Drop additionalProperties: false from typed schemas
JSON Schema 2020-12 (mandated by OpenAPI 3.1) does not let additionalProperties look through allOf, so a strict validator rejects every inherited field on the composed *ResponseModel/*Model/*PropertiesModel schemas. Most code generators silently ignore it, but the document is technically invalid and the constraint would be a lie anyway since Umbraco can grow new properties in non-major releases.

Removed from all four schema construction sites (response, content type, properties, and the interface-based fallback) and regenerated the affected snapshots.
2026-05-06 10:22:22 +02:00
Laura Neto 074d2ea9f0 Updated expected contracts following code adjustments 2026-05-06 09:49:04 +02:00
Andy Butland c45a79bcd7 Allows nulls at property reference sites without mutating any shared component schema.
Avoid unnecessary re-get of the JsonTypeInfo for the default case.
2026-05-06 08:04:35 +02:00
Laura Neto c6140ed807 Merge remote-tracking branch 'origin/v18/dev' into v18/feature/delivery-api-document-type-schema-generation-clean 2026-05-05 16:42:32 +02:00
Laura Neto 1eb0ec9755 Merge branch 'v18/dev' into v18/feature/delivery-api-document-type-schema-generation-clean
# Conflicts:
#	tests/Umbraco.Tests.Integration/Umbraco.Api.Delivery/OpenApi/ExpectedContracts/default.json
2026-05-05 16:41:33 +02:00
a50397f4e6 Elements: Clean up the remaining TODOs (#22689)
* Management API sweep

* Remove leftover comment from ContentService

* Clarify TODOs

* Use IPublishedElementCache instead of IElementCacheService in ElementPickerValueConverter

* Rename private helper for clarification

* Fix build error

* Remove "Create" from ElementService, as it was only ever used for tests

* Rename DocumentVariantStateModel to PublishableVariantStateModel in backoffice client

Refresh OpenApi.json and regenerate backend-api after the server-side enum rename, then update all client imports and usages to match.

* Client: Aliased `PublishableVariantStateModel` for each module package

* Client: Resolve circular dependencies for variant-state alias

Hoist the `UmbDocumentVariantState` and `UmbElementVariantState` aliases (re-exporting `PublishableVariantStateModel`) into dedicated `variant-state.ts` files. Internal modules now import the alias from this leaf file instead of the package's root `index.js`, breaking the 5 cycles reported by `npm run check:circular` while keeping the public API surface unchanged.

* Post-merge fixes

---------

Co-authored-by: Laura Neto <12862535+lauraneto@users.noreply.github.com>
Co-authored-by: leekelleher <leekelleher@gmail.com>
2026-05-05 16:40:14 +02:00
c7ba6506aa E2E: QA Updated acceptance tests to reflect UI changes in v18 (#22709)
* Updated webhook tests since Change the default payload type to "minimal"

* Added .skip tag for block grid area tests due to the actual issues

* Update template tests due to test helpers changes

* Updated locator for rollback button due to UI changes

* Updated locator for block edit button due to UI changes

* Updated locator for delete block icon

---------

Co-authored-by: Andreas Zerbst <73799582+andr317c@users.noreply.github.com>
2026-05-05 14:38:59 +00:00
Laura NetoandGitHub 96ae2f384f Delivery API: Drop $type discriminator from response payloads (#22710)
* Delivery API: Drop $type discriminator from response payloads

Removed [JsonDerivedType] from IApiContent and IApiContentResponse so
System.Text.Json stops emitting $type on collection endpoints and the
OpenAPI spec stops requiring a discriminator on the generic schemas,
restoring v17 behaviour. Consumers that need polymorphic responses can
still register derived types via ContentJsonTypeResolverBase.

Snapshot regenerated.

* Delivery API: Preserve cultures property order on collection responses

Added [JsonPropertyOrder(100)] to IApiContentResponse.Cultures so the
property is serialized last when the static type is the interface
(collection endpoints), matching the existing attribute on the concrete
ApiContentResponse class. Mirrors the [JsonPropertyOrder(-100)] pattern
already used for ContentType on IApiElement / ApiElement.

Snapshot regenerated.
2026-05-05 15:37:22 +02:00
f158e6601b Code Tidy: Remove unused obsoleted InstalledPackage mapping (#22713)
* Remove unused obseleted InstalledPackage mapping

* Fix up XML header documentation on PackageViewModelMapDefinition.

---------

Co-authored-by: Andy Butland <abutland73@gmail.com>
2026-05-05 12:18:25 +00:00
8e6e0e7f05 Code Tidy: Clean up further obsoleted code scheduled for removal in Umbraco 18 (ILocalizationService) (#22677)
* Remove the obsolete ILocalizationService and implementation and update all callers to non-obsolete alternatives.

* Addressed code review feedback.

* Fixed failing integration test.

---------

Co-authored-by: mole <nikolajlauridsen@protonmail.ch>
2026-05-05 11:50:45 +00:00
Jacob Overgaard d0648ac7df chore: updates references to renamed uui-css.css -> light.css file 2026-05-05 13:31:11 +02:00
c257b91443 Code Tidy: Make name non-nullable on content/element/media/member constructors (#22638)
* Remove overloads for creation of content that allow for null name.

* Add defensive null guard on create media.

* Remove unused publishedValueFallback parameter in published content models.

* Fixed failing integration tests.

---------

Co-authored-by: Kenn Jacobsen <kja@umbraco.dk>
2026-05-05 12:10:47 +02:00
Laura NetoandGitHub 6866a964f6 Elements: Add elements to the backoffice global search (#22674)
* Backoffice Element Search: add global search provider for elements

Adds an "Elements" category to the backoffice global search, scoped to
the Library section.

Server: SearchElementItemController exposes
  GET /umbraco/management/api/v1/item/element/search
backed by IEntitySearchService (DB-backed name match, mirrors the
DataType search pattern). Maps results via IElementPresentationFactory.

Client: new src/packages/elements/search/ module with a search provider,
repository, server data source, search-result-item element, and
globalSearch manifest (alias Umb.GlobalSearch.Element). Wired into the
elements package manifests. Backend SDK regenerated from OpenApi.json.

* Backoffice Element Search: surface ancestors, trashed and draft state

- New ancestors endpoint at /item/element/ancestors so result items can
  render a parent breadcrumb (uses NamedItemResponseModel to cover
  element folder ancestors).
- ElementItemResponseModel.IsTrashed added and populated by the
  presentation factory, flowing through search and item responses.
- Frontend search result item renders breadcrumb, Trashed tag with
  strike-through, and Draft tag (mirrors document search result item).

* Address PR review feedback

- Add integration test for AncestorsElementItemController (mirrors
  AncestorsDocumentItemControllerTests).
- UmbElementSearchItemModel: declare `name: string` (the search result
  contract requires it; mirrors UmbDocumentSearchItemModel).
- Element search data source: drop the empty-string fallback on `name`
  and add the same TODO comment used in the document data source.
- Add JSDoc to UmbElementSearchProvider, UmbElementSearchRepository and
  UmbElementSearchServerDataSource (matches document equivalents).

* Backoffice Element Search: export search consts and unblock isTrashed on item endpoint

- Re-export ./search/constants.js from the elements package barrel so
  UMB_ELEMENT_SEARCH_PROVIDER_ALIAS and UMB_ELEMENT_GLOBAL_SEARCH_ALIAS
  are reachable as the export-consts test expects.
- element-item.server.data-source.ts: stop hardcoding isTrashed to false
  - now that ElementItemResponseModel exposes the flag, item-based UIs
  reflect the actual trashed state.
2026-05-05 10:08:37 +00:00
Andy ButlandandGitHub 233865a1e6 Code Tidy: Clean up obsoleted code scheduled for removal in Umbraco 18 (IDomainService, IContentTypeBaseService) (#22629)
* Fix naming warning in UmbracoIntegrationTestBase.

* Fixes a namespace.

* Removed TODO for removing registrations of UserPasswordConfigurationSettings and MemberPasswordConfigurationSettings. The inheritance hierarchy of UmbracoUserManager makes this difficult and unnecessary to unpick.

* Aligned TODO with obsoletion message.

* Remove obsoleted code from IDomainService and update all callers.

* Removed obsolete members from IContentTypeBaseService.

* Addressed code review feedback.

* Fix Setup on ThreadSafetyTests.
2026-05-05 10:02:18 +00:00
877e93aec2 Elements: Add the Library section to the admin group on upgrade (#22706)
* Add the Elements section to the admin group on upgrade

* Update src/Umbraco.Infrastructure/Migrations/Upgrade/V_18_0_0/AddElementSectionForAdmins.cs

Co-authored-by: Laura Neto <12862535+lauraneto@users.noreply.github.com>

---------

Co-authored-by: Laura Neto <12862535+lauraneto@users.noreply.github.com>
2026-05-05 09:58:19 +00:00
Niels Lyngsø adf02910ab Merge branch 'main' into v18/dev 2026-05-05 10:30:57 +02: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
Niels Lyngsø 89e89a38ab add library package 2026-05-05 09:56:47 +02:00
Niels Lyngsø fa123444bd re-introduce element package 2026-05-05 09:48:59 +02:00
Niels Lyngsø 5833269196 Merge branch 'main' into v18/dev
# Conflicts:
#	src/Umbraco.Web.UI.Client/src/apps/backoffice/backoffice.element.ts
#	tests/Umbraco.Tests.AcceptanceTest/lib/helpers/ConstantHelper.ts
#	tests/Umbraco.Tests.AcceptanceTest/lib/helpers/ContentUiHelper.ts
#	tests/Umbraco.Tests.AcceptanceTest/lib/helpers/UserApiHelper.ts
2026-05-05 09:24:02 +02:00
Andy ButlandandGitHub 0c9b817372 Code Tidy: Clean up obsoleted code scheduled for removal in Umbraco 18 (IDataTypeService) (#22634)
* Remove obsolete methods from IDataTypeService and update callers.

* Fixed failing integration test and resolved code review feedback.

* Further code review feedback.

* Introduce shared helper for retrieving data type from property type.
2026-05-05 05:06:50 +00: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
Sven GeusensandGitHub 29ecd48cc1 Migrations: Removed obsoleted MigrationBase and all migrations between old and current LTS (13-17) (#22618)
* Change AddElements to a premigration

* Move AddAllowedInLibraryToContentType to premigration

* Remove old migrations and remove obsoleted migratiobase

Includes updating existing migrations and tests to AsyncMigrationBase

* Update claude files

* update xml comment

* Set correct initalstate

* More cleanup

* Remove old migration tests

* Put the test ignore on the right testcase 🙈

* cleanup async migrateasync calls without awaits in tests

* Updated InitialStateVersion
2026-05-05 06:48:16 +02:00
Andy ButlandandGitHub 6c2473aeb0 Code Tidy: Remove package validation suppression files (#22696)
Removed CompatibilitySuppressions.xml files.
2026-05-05 10:14:10 +09:00
Andy ButlandandGitHub 7e2edd4733 Dependencies: Bump selected NuGet packages to latest versions (#22693)
* Bumped selected dependencies to the latest versions.

* Resolved warning seen on dotnet restore.
2026-05-05 09:09:00 +09:00
Laura NetoandGitHub 0d6aedac7c Management API: Refactor element tree controllers to use start node filter service (#22598)
* Refactor element tree controllers to use start node filter service

Move user start node filtering logic from UserStartNodeFolderTreeControllerBase
and ElementTreeControllerBase into a dedicated ElementStartNodeTreeFilterService,
matching the pattern established for document and media trees in PR #22486.

Add a virtual TreeObjectTypes property to UserStartNodeTreeFilterService so
element trees can query both Element and ElementContainer object types.

* Address PR review feedback

* Apply PR review feedback

Replace TreeObjectType (singular) with abstract TreeObjectTypes (array).
Use static readonly arrays in concrete implementations to avoid
allocations.

Move multi-object-type test into UserStartNodeTreeFilterServiceTests
since it exercises base class behavior, not the element service
specifically.
2026-05-04 23:13:23 +02:00
Laura Neto 61ce9e70e9 Merge remote-tracking branch 'origin/v18/dev' into v18/feature/delivery-api-document-type-schema-generation-clean 2026-05-04 23:10:54 +02:00
Laura Neto ecce15c39b Drop default JsonDerivedType registrations from Delivery API interfaces
Removes the [JsonDerivedType] attributes from IApiContent,
IApiContentResponse, and IApiElement. Without them System.Text.Json
configures no polymorphism by default, so wire payloads stop carrying
$type fields and the OpenAPI spec stops emitting a discriminator on
the generic schemas - matching v17 Delivery API behaviour. Consumers
that need polymorphic serialization can still register derived types
via ContentJsonTypeResolverBase.

Snapshots regenerated.
2026-05-04 23:10:02 +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
Laura Neto 085fcda2ba Stop registering media derived types in the JSON resolver
ContentJsonTypeResolverBase.GetDerivedTypes goes back to returning
empty. Previously it registered ApiMediaWithCrops and
ApiMediaWithCropsResponse as derived types of their interfaces, which
made every consumer of the resolver (the Delivery API and webhooks)
emit a $type discriminator on media payloads, even when the typed
schema feature was disabled.

The Delivery API still needs a base schema for the typed media
schemas to extend via allOf. Since the concrete media classes are
internal to Umbraco.Infrastructure and cannot be referenced from
[JsonDerivedType] in Core, ContentTypeSchemaTransformer now builds
that base from the interface's own properties when the interface has
no [JsonDerivedType] entries. Content/element interfaces are
unaffected and keep using their declared concrete derived types.

Snapshots regenerated.
2026-05-04 20:54:40 +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
Laura Neto e0abfcb4b0 Honour Delivery API allow/deny list in typed OpenAPI schemas
ContentTypeSchemaTransformer now filters DocumentTypes through
DeliveryApiSettings.IsAllowedContentType so document types blocked
by AllowedContentTypeAliases / DisallowedContentTypeAliases no longer
leak into the polymorphic union or discriminator mapping.
2026-05-04 18:42:34 +02:00
7eb586f520 Code Tidy: Clean up further obsoleted code scheduled for removal in Umbraco 18 (IMemberService.GetMembersByPropertyValue) (#22678)
Removed obsolete methods on IMemberService, their implementations and the related tests.

Co-authored-by: Laura Neto <12862535+lauraneto@users.noreply.github.com>
2026-05-04 14:59:49 +00:00
Andy ButlandandGitHub 138e29db58 Code Tidy: Remove obsolete code scheduled for removal in Umbraco 18 (UmbracoApiController and front-end API auto-routing) (#22692)
* Remove UmbracoApiController and associated code.

* Test naming and attributes.
2026-05-04 16:13:22 +02:00
Andy ButlandandGitHub 1a87feb84d Code Tidy: Remove obsolete code scheduled for removal in Umbraco 18 (UrlSegment extension method) (#22682)
* Remove obsolete UrlSegment extension methods and update callers. Clarify obsoletion of UrlSegment property for Umbraco 19.

* Use 20 for the new obsoletions.

* Align all existing obsolete and non-obsolete calls to retrieve a URL segment to use IDocumentUrlService.

* Fix failing tests.

* Revert incorrectly updated obsoletion removal version.
2026-05-04 13:36:58 +00:00
Laura NetoandGitHub 5bd22410a6 Merge branch 'v18/dev' into v18/feature/delivery-api-document-type-schema-generation 2026-05-04 15:20:30 +02:00
Laura Neto 16f6e3a4b2 Add Delivery API client test projects 2026-05-04 15:06:00 +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
2434c3ec7b Global Elements: Implements auditLog and contentRollback kinds for History and Rollback (#22633)
* Implements `auditLog` kind for Elements

* Implements `contentRollback` kind for Elements

* Adds JSDoc comments to element rollback repository and data source

* Exports `UMB_ELEMENT_AUDIT_LOG_REPOSITORY_ALIAS` from `@umbraco-cms/backoffice/element`

Surfaces the constant through the element audit-log barrel so it's available on the public package entry, matching the documents audit-log pattern.

* Added `rollbackNotificationMessage` for Element Rollback

---------

Co-authored-by: Niels Lyngsø <niels.lyngso@gmail.com>
2026-05-04 12:20:10 +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
c725881e67 Elements: Published element extensions (#22585)
* Add element-level extension methods for variance, culture, fallback support

Published Element Extensions now support the same culture, type-checking,
equality, and creator/writer methods that were previously only available
on Published Content Extensions. Content extensions delegate to the
element versions, preserving backwards compatibility.

New element extensions (Core):
- HasCulture, IsInvariantOrHasCulture, CultureDate
- IsDocumentType (both overloads)
- IsEqual, IsNotEqual
- GetCreatorName, GetWriterName
- HasValue with IPublishedValueFallback and Fallback support

New friendly element wrappers (Web.Common):
- Name, CultureDate, CreatorName, WriterName

New non-friendly element extensions (Web.Common):
- CreatorName, WriterName (with IUserService parameter)

* Add unit tests for PublishedElement extension methods

Tests for core extensions (HasCulture, IsInvariantOrHasCulture,
CultureDate, IsDocumentType, IsEqual/IsNotEqual, GetCreatorName,
GetWriterName, HasValue with fallback) and friendly wrappers (Name,
CultureDate, CreatorName, WriterName). All mocks use MockBehavior.Strict.

* Fix empty XML doc param tag for variationContextAccessor in CultureDate

* Delegate content CreatorName/WriterName to element friendly extensions, remove redundant UserService field

* Address PR review: restore StaticServiceProvider in TearDown, use case-insensitive culture dictionary in tests

* Add remarks note about Fallback.ToAncestors not being supported at element level

* Clarify the casting for readability

---------

Co-authored-by: kjac <kja@umbraco.dk>
2026-05-04 13:30:27 +02:00
Andy Butland 51892a840c Merge branch 'v18/dev' of https://github.com/umbraco/Umbraco-CMS into v18/dev 2026-05-04 13:15:20 +02:00
Andy Butland a2fa694553 Merge remote-tracking branch 'origin/main' into v18/dev 2026-05-04 13:15:00 +02: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
Laura NetoandGitHub 4e74e33dde Templates: Update Umbraco extension template for OpenAPI route changes (#22670)
* Update Umbraco extension template for OpenAPI route changes

Following the migration from Swashbuckle to Microsoft.AspNetCore.OpenApi
in #21058, the extension template still pointed at the old Swagger URL
pattern and used outdated terminology in code comments.

- generate-client npm script now points at /umbraco/openapi/{name}.json
  instead of /umbraco/swagger/{name}/swagger.json
- generate-openapi.js renames swaggerUrl to openApiUrl and updates the
  example URL in the missing-argument error message
- UmbracoExtensionApiComposer.cs comments updated from "Swagger" to
  "OpenAPI"

* Scope custom OpenAPI document to extension's own endpoints

Without an explicit ShouldInclude predicate, Microsoft.AspNetCore.OpenApi
only includes endpoints whose ApiExplorer GroupName equals the document
name. The template's controller declared a different group name, so the
custom document was created but stayed empty (paths: []), which in turn
made npm run generate-client produce an empty TypeScript SDK.

Filter by the [MapToApi] attribute already present on the extension's
controller base, mirroring the pattern used by the Management and
Delivery API options.

* Add Microsoft.AspNetCore.OpenApi reference to Central package management

The PerProject mode of the umbraco-extension template took a direct
dependency on Microsoft.AspNetCore.OpenApi (with a long comment
explaining why) but the Central mode did not, so default Central
scaffolds failed to build with the source-generator interceptors
error. Mirror the dependency in the Central csproj block and
Directory.Packages.props.
2026-05-04 12:16:48 +02:00
2d4418b36f Code Tidy: Clean up further obsoleted code scheduled for removal in Umbraco 18 (LogFiles, legacy permissions tables, LoggerConfigExtensions) (#22679)
* Remove obsolete logger configuration extensions.

* Removed obsolete database table DTO and constants.

* Removed obsolete LogFiles constant.

* Moved SuperUserId constants obsoletion to 19.

* Remove further reference to removed table.

* Comment out reference to removed table in migration that is also for removal for 18.

* Remove further obsolete methods from LoggerConfigExtensions

* Apply suggestions from code review

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

---------

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-05-04 12:08:16 +02: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
Laura NetoandGitHub 308369452d Merge branch 'v18/dev' into v18/feature/delivery-api-document-type-schema-generation 2026-05-04 11:42:38 +02: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
Nhu DinhandGitHub b638fa0c73 E2E: Revert npm command for smokeTest (#22683)
* Revert npm command for smokeTest

* Updated audit trail for trash content
2026-05-04 15:48:20 +07: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
101acdd583 Templates: Rename "Master Template" to "Layout Template" (#21743)
* Rename "Master Template" to "Layout Template" throughout the codebase

Since Umbraco switched from WebForms to MVC, the "Master" template
terminology has been incorrect — in Razor/MVC the parent template is
called a "Layout", not a "Master page". This renames the concept across
C# models, services, repositories, the Management API, and the
backoffice frontend while preserving backward compatibility via
[Obsolete] members scheduled for removal in Umbraco 20.

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

* Remove dead template layout XML element code and rename test methods

The serializer code that wrote <Master>/<MasterAlias> elements was never
executed because Lazy<int>.IsValueCreated was always false after loading
from the database. The corresponding import code that read these elements
was equally dead since no package.xml ever contained them. Template
parent-child hierarchy is resolved from Razor Layout directives instead.

Also renames 4 test methods from "Master" to "Layout" to match the
updated terminology.

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

* Packaging: Suppress noisy log when imported Template has no Layout

A null Layout is legitimate for root layout files (e.g. `Layout = null;`)
and shouldn't be reported as "invalid". Only log when a non-null Layout
was referenced but couldn't be resolved in the import.

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

* Update acceptance tests and further references in comments.

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Sebastiaan Janssen <sebastiaan@umbraco.dk>
Co-authored-by: Andy Butland <abutland73@gmail.com>
2026-05-04 10:01:42 +02:00
Lee KelleherandGitHub ad904d6b05 Global Elements: Localize bulk publish/unpublish notifications (#22641)
* Localizes element bulk publish/unpublish notifications

* Removed the "visible on the website" part from localizations for Elements.
2026-05-04 04:35:30 +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
Andy ButlandandGitHub b499a0e121 Code Tidy: Clean up obsoleted code scheduled for removal in Umbraco 18 (IMemberGroupService) (#22632)
Remove obsolete methods on IMemberGroupService and update callers.
2026-05-03 09:47:38 +02: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
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
Kenn JacobsenandGitHub f08bd93793 Cherry-picked the missing constant for "unroutable" (#22672) 2026-05-01 15:49:16 +02:00
4543856ce1 Elements: Clear element entity cache on content type changes (#22362)
fix(core): clear element entity cache on content type changes

The ContentTypeCacheRefresher clears IContent isolated cache when a
content type changes, but did not clear IElement cache. This caused
stale element entities to be returned after modifying property type
variation settings (e.g. enabling vary-by-culture), leading to 500
errors when saving elements.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Laura Neto <12862535+lauraneto@users.noreply.github.com>
2026-05-01 13:09:09 +00:00
Laura Neto f97db75db0 Delivery API: Generate typed OpenAPI schemas per content type 2026-05-01 10:48:27 +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
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
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
5a545fa122 Open API: Use Microsoft.AspNetCore.OpenApi for Open API document generation (#21058)
* Uninstall `Swashbuckle.AspNetCore` and install `Microsoft.AspNetCore.OpenApi`

Also installed `Swashbuckle.AspNetCore.SwaggerUI` for now to use as UI only.

* Registered UI and removed or commented out Swashbuckle specific code

* Started configuring the different Open API documents

* Started moving configuration

* Simplifying configuration

* Added missing configuration for the Delivery API

* Added missing configurations for Management API

Still missing polymorphism settings for both APIs

* Adjust Umbraco Extension template with OpenApi changes

* Handle sub types in open api document generation

* Renaming mime types transformer to align with others

* Added discriminator configuration

* Reference Umbraco.Cms.DevelopmentMode.Backoffice from integration tests project to avoid models mode exception being logged in tests

* Now configuring and using the HTTP json options instead of having custom transformers for handling enums and polymorphism

* Fixes to examples

* Update OpenAPI packages

* Mark most transformers as internal

* Simplify adding backoffice security requirements to your API

* Fix missing required properties

* Re-order transformers to fix missing notification headers

* Fix most build errors after regenerating client

* Fix mime types transformer being applied to Management API

* Additional fixes

* Additional fixes to file response types

* Configure Swagger UI documents

* Clear server list

* Sort APIs in UI

* Re-introduce schema handlers and fix issue with nullable enum schema name

* Simplify examples

* Small optimization

* Simplify nullability check in RequireNonNullablePropertiesSchemaTransformer

* Remove unused property

* Small fixes suggested by Claude

* Undo unintended space changes

* Add unit tests for OpenAPI transformers

🤖 Generated with [Claude Code](https://claude.com/claude-code)

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

* Add unit tests for additional OpenAPI transformers

- RequireNonNullablePropertiesSchemaTransformer (7 tests)
- BackOfficeSecurityRequirementsTransformer (10 tests)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

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

* Rename SwaggerGen classes to OpenApi for consistency

- Rename ConfigureUmbracoDeliveryApiSwaggerGenOptions to ConfigureUmbracoDeliveryApiOpenApiOptions
- Rename ConfigureUmbracoMemberAuthenticationDeliveryApiSwaggerGenOptions to ConfigureUmbracoMemberAuthenticationDeliveryApiOpenApiOptions
- Rename ConfigureUmbracoManagementApiSwaggerGenOptions to ConfigureUmbracoManagementApiOpenApiOptions
- Rename SwaggerRouteTemplatePipelineFilter to OpenApiRouteTemplatePipelineFilter
- Update DI registrations to use new class names

🤖 Generated with [Claude Code](https://claude.com/claude-code)

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

* Update OpenAPI contract test for Microsoft.AspNetCore.OpenApi

Update expected Delivery API OpenAPI contract to reflect changes from
the migration to Microsoft.AspNetCore.OpenApi:

- OpenAPI version 3.0.4 → 3.1.1
- Nullable types now use type array format (OpenAPI 3.1 style)
- Polymorphic types use anyOf with discriminator
- Security moved from header parameter to securitySchemes
- Removed unnecessary oneOf wrappers around single $ref

🤖 Generated with [Claude Code](https://claude.com/claude-code)

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

* Disable Models Builder in integration tests by default

* Rename Swagger references to OpenApi for consistency

- Rename SwaggerIsEnabled to OpenApiIsEnabled
- Rename SwaggerRouteTemplate to OpenApiRouteTemplate
- Rename SwaggerUiRoutePrefix to OpenApiUiRoutePrefix
- Rename SwaggerUiConfiguration to ConfigureOpenApiUI
- Rename swaggerPipelineFilter variable to openApiPipelineFilter
- Update code comments from "Swagger JSON" to "OpenAPI specification"

🤖 Generated with [Claude Code](https://claude.com/claude-code)

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

* Re-generate Management API open api doc and UI client after merge

* Add reference in comment to additional PR to fix file return types schema

* Fix Open API validation errors

* OpenAPI: Replace ISchemaIdHandler/ISchemaIdSelector with static UmbracoSchemaIdGenerator

Remove the DI-based schema ID handler/selector pattern and replace with a
static UmbracoSchemaIdGenerator utility class. This allows both Umbraco code
and external consumers to call the schema ID generation logic directly, which
is useful since the Microsoft OpenAPI package's schema selectors only apply
to Umbraco's own OpenAPI documents.

- Remove ISchemaIdHandler, ISchemaIdSelector interfaces and implementations
- Add static UmbracoSchemaIdGenerator.Generate() method
- Update ConfigureUmbracoOpenApiOptionsBase to use UmbracoSchemaIdGenerator directly
- Remove constructor dependencies from API options classes
- Add unit tests for UmbracoSchemaIdGenerator and CreateSchemaReferenceId

* Rename CustomOperationIdsTransformer to UmbracoOperationIdTransformer and make public

- Rename class to better reflect its purpose as Umbraco's operation ID transformer
- Change visibility from internal to public so it can be used by external consumers
- Update XML documentation to clarify usage for custom OpenAPI configurations

* OpenAPI: Update Delivery API contract test for new document format

Update expected OpenAPI output to include explicit empty values in
examples and consistent array formatting in security requirements.

* OpenAPI: Remove obsolete DocumentInclusionSelector abstraction

The document inclusion logic is now handled directly by
ConfigureUmbracoOpenApiOptionsBase.ShouldInclude(), making
the separate IDocumentInclusionSelector abstraction unnecessary.

* OpenAPI: Reorganize Management API OpenApi folder structure

- Move transformers to OpenApi/Transformers subfolder
- Move OpenApiOptionsExtensions from Extensions to OpenApi folder
- Update namespaces accordingly:
  - Umbraco.Cms.Api.Management.OpenApi.Transformers (transformers)
  - Umbraco.Cms.Api.Management.OpenApi (extensions)

* OpenAPI: Add ExcludeFromDefaultOpenApiDocument attribute

- Add [ExcludeFromDefaultOpenApiDocument] attribute for excluding controllers from the default OpenAPI document
- Make ShouldInclude method protected virtual in ConfigureUmbracoOpenApiOptionsBase for extensibility
- Override ShouldInclude in ConfigureDefaultApiOptions to check for the exclusion attribute

* OpenAPI: Add UmbracoOpenApiOptions for configuring OpenAPI routes

Add UmbracoOpenApiOptions configuration class to allow customizing:
- Enabled: Enable/disable OpenAPI and Swagger UI (default: non-production)
- RouteTemplate: Route template for OpenAPI JSON documents
- UiRoutePrefix: Route prefix for Swagger UI

Umbraco sets defaults via Configure, users can override via PostConfigure.
Simplify OpenApiRouteTemplatePipelineFilter to use options directly.

* Pipeline filters: Add OnPreMapEndpoints and rename OnEndpoints to OnPreEndpoints

- Add OnPreMapEndpoints method to IUmbracoPipelineFilter for registering
  endpoints inside UseEndpoints without calling UseEndpoints twice
- Rename OnEndpoints to OnPreEndpoints (with backward-compatible default)
- Add PreMapEndpoints and PreEndpoints properties to UmbracoPipelineFilter
- Mark OnEndpoints and Endpoints as obsolete (removal in Umbraco 19)
- Update UmbracoApplicationBuilder to call OnPreMapEndpoints inside UseEndpoints
- Remove redundant UseEndpoints() call from BackOfficeManagementApiFilter
- Update LoadTestController to use PreMapEndpoints instead of Endpoints

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

* OpenAPI: Move MapOpenApi to PreMapEndpoints hook

Move OpenAPI endpoint mapping from PostPipeline to PreMapEndpoints
to avoid calling UseEndpoints twice in the pipeline.

* OpenAPI: Rename URL paths from swagger to openapi

- Change OpenAPI UI and document URLs from /umbraco/swagger to /umbraco/openapi
- Rename OAuth client constant from Swagger to OpenApiUi (value kept as
  umbraco-swagger for backwards compatibility with existing DB registrations)
- Update display name to "Umbraco OpenAPI access"
- Add DefaultUiEnabled option to allow disabling the default UI while
  keeping OpenAPI documents available (enables use of alternative UIs)
- Update MiniProfiler ignored path

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

* OpenAPI: Update Microsoft.AspNetCore.OpenApi to 10.0.2

* OpenAPI: Add AddOpenApiDocumentToUi extension method

Adds a public extension method to simplify adding OpenAPI documents to the
UI document selector. This respects the configured UmbracoOpenApiOptions
route template, so users don't need to hardcode paths.

The documentTitle parameter is optional and defaults to the documentName.

Also updates the UmbracoExtension template to use the new method and
fixes the documentation URL reference.

* OpenAPI: Make OpenApiRouteTemplatePipelineFilter internal

The class has no extension points (all methods are private static) and
customization is now done via UmbracoOpenApiOptions instead.

* OpenAPI: Rename DeliveryApiSecurityFilter to DeliveryApiSecurityTransformer

Aligns naming with other OpenAPI transformers for consistency.

* OpenAPI: Simplify Delivery API member authentication configuration

Replace ConfigureUmbracoMemberAuthenticationDeliveryApiOpenApiOptions with
a simpler AddDeliveryApiOpenApiMemberAuthentication() extension method on
IServiceCollection. This hides implementation details and provides a cleaner
API for users to enable member authentication in the Delivery API OpenAPI document.

* OpenAPI: Add reference to proposal for custom JSON options support

* Move Delivery API transformers to OpenApi/Transformers folder

Aligns the folder structure with the Management API project.

* Update OpenAPI contract tests to use new URL format

Changed from /swagger/{name}/swagger.json to /openapi/{name}.json

* Apply suggestions from code review

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

* Update src/Umbraco.Cms.Api.Delivery/DependencyInjection/UmbracoBuilderExtensions.cs

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

* Fix IAuthorizationService injection detection in BackOfficeSecurityRequirementsTransformer

- Fix bug where parameter.GetType() was used instead of parameter.ParameterType,
  causing the IAuthorizationService injection check to always return false
- Replace magic number with BaseAuthorizeAttributeCount constant
- Improve comments explaining the 403 response logic
- Add test for IAuthorizationService injection detection

* Remove unnecessary InterceptorsNamespaces from API projects

* Remove default implementations from IUmbracoPipelineFilter methods

* Update documentation for Microsoft.AspNetCore.OpenApi migration

- Update CLAUDE.md files to reflect the migration from Swashbuckle to Microsoft.AspNetCore.OpenApi for document generation
- Update URL paths from /umbraco/swagger/ to /umbraco/openapi/
- Rename swaggerPath variables to openApiPath in test files
- Update references to removed types (SchemaIdHandler, OperationIdHandler, etc.) with their new equivalents (UmbracoSchemaIdGenerator, UmbracoOperationIdTransformer)
- Remove outdated technical debt reference to deleted SwaggerDocumentationFilterBase

* Update Swashbuckle.AspNetCore.SwaggerUI to 10.1.2

Fixes browser caching behavior and document URL serialization issues.

* Refactor OpenAPI contract tests with validation

- Add OpenAPI spec validation for both Delivery and Management APIs
- Delivery API: Store expected contract in external JSON file for regression testing
- Management API: Compare generated contract against expected contract endpoint
- Organize Delivery API tests under OpenApi/ subdirectory
- Auto-generate Delivery API contract file if it doesn't exist

* Update ElementReferenceResponseModel type reference after OpenAPI regeneration

* Add discriminator values to Delivery API polymorphic JSON serialization

ConfigureJsonPolymorphismOptions now passes derivedType.Name as the
discriminator value for each JsonDerivedType, ensuring the $type property
is present in responses and the OpenAPI schema is valid.

* Move Delivery API OpenAPI contract tests to Umbraco.Api.Delivery folder

* Update Microsoft.AspNetCore.OpenApi to 10.0.3 and Swashbuckle.AspNetCore.SwaggerUI to 10.1.4

* Use JsonDerivedType attributes for Delivery API polymorphic serialization

Move discriminator configuration from ContentJsonTypeResolverBase to
JsonDerivedType attributes on the interfaces. This is the standard STJ
approach and keeps the resolver available for custom overrides only.

* Add OpenAPI test for custom derived type extensibility

Extract shared test infrastructure into OpenApiTestBase and add
OpenApiCustomDerivedTypeTest to verify the OpenAPI spec remains valid
when a consumer registers a custom derived type via
ContentJsonTypeResolverBase.GetDerivedTypes.

* Fix OpenAPI contract test failing on CI due to ContinuousIntegrationBuild path normalization

[CallerFilePath] embeds a compile-time source path that gets normalized to /_/... on Azure DevOps
agents when ContinuousIntegrationBuild=true. At runtime the expected contract file is not found at
that path, causing the test to attempt Directory.CreateDirectory("/_/...") which fails with
permission denied.

Fix by reading contract files from the output directory (CopyToOutputDirectory) instead of the
compile-time source path. The [CallerFilePath] approach is kept only for writing new contracts
during local development, wrapped in a try/catch so it fails gracefully on CI.

* Bump Swashbuckle.AspNetCore.SwaggerUI to 10.1.7

* Remove duplicate InternalsVisibleTo for Umbraco.Tests.UnitTests

* Extract ReplaceOpenApiSchemaService into shared Api.Common helper

Deduplicates the internal OpenApiSchemaService replacement logic
between Management API and Delivery API into a single internal
extension method in Umbraco.Cms.Api.Common. Uses assembly and type
name checks derived from a public type (OpenApiOptions) instead of
hardcoded strings for safer matching.

* Tighten visibility and improve DI extension structure

- Mark FixFileReturnTypesTransformer as internal (temporary workaround)
- Mark AddUmbracoApiOpenApiUI and AddUmbracoApi as internal
- Rename AddUmbracoApi to AddUmbracoOpenApiDocument on IUmbracoBuilder
- Move AddOpenApiDocumentToUi to OpenApiServiceCollectionExtensions
- Encapsulate ReplaceOpenApiSchemaService inside AddUmbracoOpenApiDocument
  as an optional jsonOptionsName parameter

* Regenerate OpenApi.json to fix duplicate document patch endpoint

* Move MimeTypesTransformer to shared base and respect [Consumes]

Moves the MIME type filtering from a Delivery API-only document
transformer to a shared operation transformer in Api.Common. When
[Consumes] is present, replaces content types with exactly what it
declares (fixing application/json-patch+json on the patch endpoint).
Otherwise strips non-application/json types. Regenerates OpenApi.json
and client SDK.

* Move Umbraco-specific transformers from shared base to API configs

RequireNonNullablePropertiesSchemaTransformer, FixFileReturnTypes
Transformer, and MimeTypesTransformer are now registered only in
the Management and Delivery API configs. The default API document
(used for consumer endpoints) no longer applies these opinionated
transformers.

* Clarify XML doc for UmbracoOpenApiOptions.Enabled

* Use alphabetically-first tag across all operations for stable path sorting

* Update MimeTypesTransformer tests for operation transformer interface

* Reference dotnet/aspnetcore#66340 in ReplaceOpenApiSchemaService docs

* Add unit tests to verify OpenApiSchemaServiceExtensions usage of internal types.

* Bumped Microsoft.AspNetCore.OpenApi from 10.0.4 to 10.0.6 to match Directory.Packages.props and removed unnecessary Swashbuckle reference.

* Fix indentation.

* Defensively handle a non-integer status code response key in ResponseHeaderTransformer.

* Use TryGetValue in RequireNonNullablePropertiesSchemaTransformer to avoid potential KeyNotFoundException.

* Additional tests and clarifying comments.

* Revert accidental local dev changes to Program.cs, Web.UI.csproj and StaticAssets.csproj.

* Tighten visibility of OpenAPI configuration and transformer classes to internal

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Andy Butland <abutland73@gmail.com>
2026-04-30 19:12:44 +00: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
Nhu DinhandGitHub 8a50f80f28 E2E: QA Updated acceptance tests for Global elements to match the UI changes (#22511)
* Updated element creation step due to UI changes

* Updated element creation due to UI changes - cont

* Removed unused locator

* Updated locator for elementTreeItem

* Updated tests for library to match the UI changes

* Updated locator for elementVariantDropdown

* Updated tests for element permission and start nodes

* Removed @smoke tags

* Make tests run in the pipeline

* Added comment for failing tests

* Updated tests for element start nodes as the front-end does not support adding a element as start nodes

* Fix flaky tests

* Fixed comment
2026-04-30 13:08:06 +00: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ø 0908586e89 update package-lock with version number 2026-04-30 10:21:29 +02:00
Andy Butland e6f53b9d30 Bump version to 17.3.5. 2026-04-30 10:14:57 +02:00
Andy ButlandandGitHub 6a754894d2 Code Tidy: Clean up further obsoleted code scheduled for removal in Umbraco 18 (IEmailSender, MemberConfigurationResponseModel, MediaPermissions) (#22642)
* Removed obsolete methods and default implementations on IEmailSender.

* Removed the obsolete and unused MemberConfigurationResponseModel.

* Remove the obsolete MediaPermissions and ensure test coverage is maintained.
2026-04-30 09:56:31 +09:00
Sven GeusensandGitHub 24b25f684a Enable single blocklist migration (#22627)
* Fix incorrect frontend propertyeditor alias

* Fix early return mistake

* Enable the plan

* Add memberTypes to the lookup
2026-04-29 15:54:20 +02:00
leekelleher 99c865e0bd Fix for broken UI test 2026-04-29 14:48:24 +01:00
Andy Butland b68624a961 Merge branch 'main' into v18/dev 2026-04-29 13:55:19 +02:00
Andy Butland cc373296df Remove inadvertently committed research files from source control 2026-04-29 13:52:48 +02:00
leekelleher 2e84d11c53 Merge branch 'main' into v18/dev
# Conflicts:
#	src/Umbraco.Cms.Api.Management/OpenApi.json
#	src/Umbraco.Web.UI.Client/src/packages/core/backend-api/sdk.gen.ts
#	src/Umbraco.Web.UI.Client/src/packages/core/backend-api/types.gen.ts
2026-04-29 12:50:06 +01: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
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
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
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
Andy Butland 94d94e9f46 Merge branch 'main' into v18/dev 2026-04-28 10:34:36 +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
Sven GeusensandGitHub 8b504a2916 Change Element migrations to a premigrations (#22617)
* Change AddElements to a premigration

* Move AddAllowedInLibraryToContentType to premigration
2026-04-27 16:30:07 +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ø 31dfd52b72 todo comments for future 2026-04-24 16:52:02 +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
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
32ec824dab Document Types: Show message for non-applicable Element Type settings (#22396)
* Conditionally render history & structure settings

* Show "not applicable" message instead of hiding the settings

* Refactored to reuse `#renderElementDoesNotSupport()`

* Modified "Allow in Library"

to display a message instead of hiding the field.

---------

Co-authored-by: leekelleher <leekelleher@gmail.com>
2026-04-24 11:02:25 +00:00
Kenn JacobsenandGitHub 5e4791ea2f Fix the NullableLanguageId build errors after merge (#22589) 2026-04-24 09:42:47 +00: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
Andy Butland 53d46034d5 Merge branch 'main' into v18/dev 2026-04-23 17:06:22 +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
Andy ButlandandGitHub 0bf2d04885 Hosting: Make IHostingEnvironment.ApplicationMainUrl nullable (#22558)
* Make ApplicationMainUrl on IHostingEnvironment nullable.
Update usage in HttpsCheck healthcheck and add unit tests to verify refactor.

* Improves XML documentation for the property.
2026-04-23 06:21:14 +00: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
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
Andy Butland 2daf9dae80 Merge branch 'main' into v18/dev 2026-04-22 14:34:53 +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
Andy Butland 23ad35b7d7 Merge branch 'v18/dev' of https://github.com/umbraco/Umbraco-CMS into v18/dev 2026-04-22 12:09:09 +02:00
Andy Butland 99a94fbb93 Post-merge updates for elements. 2026-04-22 12:08:48 +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
Andy Butland 3b3a11f31b Merge branch 'main' into v18/dev 2026-04-22 11:25:20 +02:00
f981502781 Dependencies: Revert NUnit 4 upgrade to unblock integration tests (#22562)
Revert "Dependencies: Upgrade NUnit and related test dependencies to latest major versions (#22155)"

This reverts commit 7014f9a125 on v18/dev
to resolve the Part3Of4 SQL Server integration test nightly hangs that
started around 2026-04-10.

Root cause was confirmed by hang-dump analysis: a sync-over-async call
in ContentCacheRefresher.HandleMemoryCache
(.GetAwaiter().GetResult() on an async cache method) that NUnit 3's
pumping synchronization context had been quietly completing on the test
thread. NUnit 4 dropped that behaviour, so the continuation now requires
a free thread-pool thread; under CI conditions it deadlocks.

Reverting #22155 on a branch was verified to make the nightly pass.
This is a temporary rollback to unblock v18; the proper fix is to make
ContentCacheRefresher.Refresh async end-to-end, tracked separately.

Additional adjustments beyond the pure revert to keep the branch
compiling:

- CoreConfigurationHttpTests.cs: added `using Umbraco.Cms.Core.Services;`
  for IUserService (referenced by post-#22155 code unaffected by the
  revert).
- ContentVersionCleanupServiceTest.cs: merged imports so both
  AutoFixture.NUnit3 (from revert) and Microsoft.Extensions.Options
  (from unrelated later commit) stay.
- UdiTests.cs and ContentPermissionResourceTests.cs: removed unused
  `using NUnit.Framework.Legacy;` (namespace introduced in NUnit 4).
- BackOfficeAuthorizationInitializationMiddlewareTests.cs: replaced
  `[CancelAfter(5000)]` with its NUnit 3 equivalent `[Timeout(5000)]`.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 11:04:27 +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
Andreas ZerbstandGitHub 40a505dbc3 Integration Tests: Split Windows ManagementApi shard to avoid LocalDb memory pressure (#22559)
* Split Windows ManagementApi shard to avoid LocalDb memory pressure

* Fixed filter
2026-04-22 08:54:07 +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
Andy Butland 9adac463e9 Merge branch 'main' into v18/dev 2026-04-21 15:56:36 +02: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
Andy ButlandandGitHub 858710cfec Output Caching: Evict cached documents when a related element is published (#22496)
Evict documents from delivery API and website output cache when related element is published.
2026-04-21 14:10:55 +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
Andy Butland 7de26aee7e Merge branch 'main' into v18/dev 2026-04-21 13:15:22 +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
Andy Butland 558a6bd724 Merge branch 'main' into v18/dev 2026-04-21 11:23:10 +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
Laura Neto b6a048e4f6 Merge branch 'main' into v18/dev
# Conflicts:
#	src/Umbraco.Infrastructure/Security/BackOfficeUserStore.cs
#	src/Umbraco.Web.UI.Client/src/assets/lang/en.ts
#	tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/TrackRelationsTests.cs
2026-04-21 11:13:15 +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
498c1ce2b8 Hybrid Cache: Element cache (#22369)
* Implement ElementCacheService with HybridCache backing and database cache support

Fully implements ElementCacheService as the elements equivalent of DocumentCacheService,
backed by Microsoft HybridCache (L1 in-memory + L2 distributed) with database cache table
persistence via cmsContentNu.

Key changes:
- ElementCacheService: full implementation with HybridCache, draft/published separation,
  converted element L0 cache, cache tagging, preview service support, and seeding infrastructure
- IDatabaseCacheRepository: added element CRUD methods (Get/Refresh/Rebuild) with SQL queries
  using ElementDto/ElementVersionDto
- IContentCacheService: extracted common base interface shared by Document, Media and Element
  cache services (8 shared methods including Seed, Rebuild, memory cache operations)
- CacheRefreshingNotificationHandler: added element notification handling, content type changes
  now route to element or document service based on IsElement, refactored to single-pass
  classification with shared RefreshCacheForContentTypeChanges method
- ElementRefreshNotification: new notification wired to ElementRepository.OnUowRefreshedEntity
- Renamed document-specific methods for clarity (GetContentSource -> GetDocumentSource,
  RefreshContent -> RefreshDocument, CreateContentNodeKit -> CreateDocumentNodeKit,
  RebuildContentDbCache -> RebuildDocumentDbCache)
- Renamed shared DTOs (CacheRebuildDocumentDto -> CacheRebuildPublishableContentDto) since
  they're used by both documents and elements
- Extracted shared RebuildPublishableDbCache method to eliminate duplication between document
  and element rebuild logic

* Add element navigation service, publish status tracking, and breadth-first seeding

Adds the infrastructure needed for element cache seeding:

- ElementNavigationService: provides tree traversal for elements, following the
  same pattern as DocumentNavigationService/MediaNavigationService
- Split PublishStatusService into an abstract base class with DocumentPublishStatusService
  and ElementPublishStatusService subclasses, each with their own interfaces
  (IDocumentPublishStatusQueryService, IElementPublishStatusQueryService, etc.)
- ElementBreadthFirstKeyProvider: seeds the element cache on startup by traversing
  the element tree breadth-first, filtering out unpublished elements
- Element publish status is initialized on startup and kept in sync via
  ElementCacheRefresher
- Old IPublishStatusQueryService/IPublishStatusManagementService interfaces kept
  as obsolete for backward compatibility
- Non-breaking constructor changes for ContentCacheRefresher, DocumentUrlService,
  ApiContentRouteBuilder via obsolete constructor overloads

* Fix element CacheNodeFactory to set IsDraft from preview parameter

CacheNodeFactory.ToContentCacheNode(IElement, bool preview) was hardcoding
IsDraft = false instead of using the preview parameter. This caused
RefreshElementAsync to never write draft cmsContentNu rows, because
DatabaseCacheRepository.RefreshElementAsync skipped the draft write when
IsDraft was false.

* Use ElementTree lock instead of ContentTree in ElementCacheService

RefreshMemoryCacheAsync was using Constants.Locks.ContentTree instead of
Constants.Locks.ElementTree for the read lock.

* Add ElementCacheServiceTests and fix PublishStatusServiceTests for abstract base

- ElementCacheServiceTests: 9 integration tests covering draft/published retrieval,
  rebuild, delete, and RefreshElementAsync behavior
- Updated PublishStatusServiceTests to use DocumentPublishStatusService instead of
  the now-abstract PublishStatusService

* Add IPublishedElementCache facade for public element cache access

Introduces the public-facing element cache interface and implementation,
following the same pattern as IPublishedContentCache/IPublishedMediaCache.

- IPublishedElementCache: async-only interface (no legacy sync methods)
- ElementCache: facade delegating to IElementCacheService
- Added Elements property to ICacheManager, IUmbracoContext, and their
  implementations

* Add ElementHybridCacheTests and ElementHybridCacheElementTypeTests

Integration tests exercising the full element cache pipeline via
IPublishedElementCache:

ElementHybridCacheTests (7 tests):
- Draft/published retrieval by key
- Unpublished element not accessible without preview
- Draft of published element accessible
- Updated draft element reflects changes
- Deleted element removed from cache
- Element name accessible

ElementHybridCacheElementTypeTests (3 tests):
- Structural type change removes property from cached element
- Non-structural type change preserves property values
- Element removed from cache when element type is deleted

* Fix element navigation to include containers and support breadth-first seeding

The element tree contains both elements and containers (folders) with different
object types. The navigation service now queries both object types to build
the full tree hierarchy.

- Added multi-objectType overloads to INavigationRepository and
  ContentNavigationRepository using LEFT JOIN to support nodes without
  content rows (containers)
- Single-objectType methods now delegate to the multi-objectType implementation
- ElementNavigationService queries both Element and ElementContainer object types
- ElementBreadthFirstKeyProvider traverses containers without seeding them,
  only counting published elements toward the seed limit
- Added ElementBreadthFirstKeyProviderTests (9 tests) including container
  traversal scenarios

* Add ElementContentTypeSeedKeyProvider for content-type-based element seeding

Seeds elements whose content types match the configured CacheSettings.ContentTypeKeys,
mirroring the existing ContentTypeSeedKeyProvider for documents. Both providers read
from the same configuration list — document type keys seed documents, element type
keys seed elements.

* Fix ContentNavigationServiceTest mocks for multi-objectType repository overload

The single-type GetContentNodesByObjectType(Guid) now delegates to the
multi-type overload. Updated test mocks to match the IEnumerable<Guid>
signature, verifying exactly one key containing Constants.ObjectTypes.Document.

* Skip Cannot_Get_Published_Again_After_Trashing test

Trashing does not clear the published cache — this is a pre-existing issue
that also affects documents. When a cached item is trashed, the HybridCache
entry remains because RefreshMemoryCacheAsync does not remove entries when
the database returns null for trashed items.

* Replace unsafe casts with StaticServiceProvider in obsolete constructors

The obsolete constructors in ApiContentRouteBuilder and DocumentUrlService
were using direct casts from IPublishStatusQueryService to
IDocumentPublishStatusQueryService, which would fail at runtime for
external consumers compiled against pre-v18 binaries. Use
StaticServiceProvider.Instance.GetRequiredService instead, consistent
with the pattern in ContentCacheRefresher.

* Remove duplicate XML doc summary in GetElementCultureDataForNodes

* Add obsolete constructors for backward compatibility

Preserve the old constructor signatures for CacheManager,
NavigationInitializationNotificationHandler, and
PublishStatusInitializationNotificationHandler so that external
consumers compiled against pre-element-cache versions don't break.
New dependencies are resolved via StaticServiceProvider.

* Pass cancellationToken to ExistsAsync in ElementCacheService.SeedAsync

* Fix DocumentUrlServiceTests to use IDocumentPublishStatusQueryService

* Trigger Build

* Address PR review feedback

- Rename HandlePublishedAsync to HandlePublishStatusAsync in
  ContentCacheRefresher for consistency with ElementCacheRefresher
- Make ElementCacheRefresher.HandlePublishStatusAsync async to align
  with ContentCacheRefresher's pattern
- Replace inline comments with #region blocks in IDatabaseCacheRepository
- Fix double enumeration in DocumentCacheService.SeedAsync and
  ElementCacheService.SeedAsync by materializing to List before logging

* Invalidate element cache entries when trashed

Apply the same fix from #22451 (documents/media) to elements:
- ElementCacheService.RefreshElementAsync: early-return for trashed
  elements, deleting from the database cache and removing from memory.
- ElementCacheService.RefreshMemoryCacheAsync: add symmetric else
  branches so memory cache entries are removed when the database cache
  has no corresponding draft or published node (self-healing).
- Re-enable Cannot_Get_Published_Again_After_Trashing integration test.

* Move element trash cache tests to ElementHybridCacheTests

Move Cannot_Get_Trashed_As_Published and
Cannot_Get_Published_Again_After_Trashing from
ElementPublishingServiceTests to ElementHybridCacheTests where they
belong — these test cache invalidation, not publishing behavior.

Add Cannot_Get_Published_Elements_After_Folder_Trashed to verify that
trashing an element folder clears its child elements from the published
cache.

* Add element hybrid cache variant tests

Add ElementHybridCacheVariantsTests covering culture variant behavior
for the element cache: variant property values per culture, invariant
property consistency across cultures, single culture updates, single
culture publishing, and draft access to both cultures.

Add isElement parameter to
CreateContentTypeWithTwoPropertiesOneVariantAndOneInvariant to support
creating variant element types without a separate builder method.

* Rename IPublishedElementCache.GetByIdAsync to GetByKeyAsync

Align with the codebase convention where Id refers to integer
identifiers and Key refers to GUID identifiers.

* Align IDocumentPublishStatusQueryService method names with element equivalent

Add IsPublished and IsPublishedInAnyCulture to
IDocumentPublishStatusQueryService to match
IElementPublishStatusQueryService naming.

Keep IsDocumentPublished and IsDocumentPublishedInAnyCulture as obsolete
default implementations delegating to the new methods, since
IPublishStatusQueryService (which exposes these names) ships on main.

Update all internal callers to use the new names.

* Keep INNER JOIN for document/media navigation queries

Only use LEFT JOIN when the query includes container types (e.g.
element containers) which don't have umbracoContent rows. Documents
and media always have content rows, so INNER JOIN preserves query
optimizer hints for those queries.

* Consolidate breadth-first seed key provider logic into base class

Make GetSeedKeys virtual on BreadthFirstKeyProvider and introduce
ShouldSeed and ShouldTraverseChildren hooks so subclasses only need
to override filtering logic instead of duplicating the entire
traversal.

- Document: overrides ShouldSeed to filter unpublished nodes
- Element: overrides ShouldSeed + ShouldTraverseChildren (always
  traverse, since containers may have published children)
- Media: uses base defaults (seed and traverse everything)

Removes the 'new' hiding pattern and the V16 TODO.

* Revert "Rename IPublishedElementCache.GetByIdAsync to GetByKeyAsync"

This reverts commit 139d66776f.

---------

Co-authored-by: Andy Butland <abutland73@gmail.com>
2026-04-20 15:08:10 +00: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
Andy Butland 2128aba603 Merge branch 'main' into v18/dev 2026-04-20 11:53:16 +02: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
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
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
leekelleher 3de50dc707 Merge branch 'main' into v18/dev
# Conflicts:
#	src/Umbraco.Web.UI.Client/mocks/data/sets/default/media-type.data.ts
#	src/Umbraco.Web.UI.Client/mocks/data/sets/default/member-type.data.ts
#	src/Umbraco.Web.UI.Client/mocks/db/document-type.db.ts
#	src/Umbraco.Web.UI.Client/mocks/msw-handlers/document-type/structure.handlers.ts
#	src/Umbraco.Web.UI.Client/src/mocks/browser-handlers.ts
#	src/Umbraco.Web.UI.Client/src/mocks/data/utils/entity/entity-recycle-bin.ts
2026-04-17 00:53:18 +01: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
fe8c25576e Collection Action: Refactor to use extension-with-api-slot (#21974)
* Use API-enabled slot for collection actions.

* Refactor collection actions; add APIs & button folder.

* Fixed relative import.

* Add collection create action API and UI updates.

* Mark UmbExtensionApiInitializer as type import

* Update imports.

* Add additionalOptions to collection create

---------

Co-authored-by: Niels Lyngsø <nsl@umbraco.dk>
2026-04-16 10:18:11 +02:00
Andy Butland da486ca841 Merge branch 'main' into v18/dev 2026-04-16 09:39:15 +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
Laura NetoandGitHub 8e1c6c7a39 Document Types: Prevent disabling isElement when elements of that type exist (#22454)
* Document Types: Prevent disabling isElement when elements of that type exist

Mirrors the existing document-to-element guard: switching an element type
to a document type is now blocked when elements of that type exist. Adds
ElementToDocumentHasNoContentAsync to IElementSwitchValidator and a new
ContentTypeOperationStatus.InvalidElementFlagElementHasContent mapped to
a BadRequest in the document type controller.

* Address PR review feedback for isElement guard

Extract shared HasNoContentNodesAsync helper in ElementSwitchValidator
to deduplicate DocumentToElement and ElementToDocument checks. Make
WithAllowedInLibrary conditional on isElement in test setup.

* Add end-to-end integration tests for element switch validation

Add three tests to ContentTypeEditingServiceTests that verify
UpdateAsync returns the correct operation status when element
flag changes are blocked: document-to-element with existing
content, element-to-document with existing elements, and
element-to-document when used in block structures.

* Remove default interface implementation for ElementToDocumentHasNoContentAsync

Per review feedback: custom implementations of IElementSwitchValidator are unlikely, and a default implementation hides the fact that changes to the real implementation would need to be mirrored here. Accept the small breaking change for a clearer upgrade path.
2026-04-15 17:33:10 +02:00
Niels Lyngsø e40694ff9d Merge branch 'main' into v18/dev
# Conflicts:
#	src/Umbraco.Web.UI.Client/src/packages/core/tree/data/unique-tree-store.ts
#	src/Umbraco.Web.UI.Client/src/packages/data-type/tree/data-type-tree.store.ts
#	src/Umbraco.Web.UI.Client/src/packages/dictionary/tree/dictionary-tree.store.ts
#	src/Umbraco.Web.UI.Client/src/packages/documents/document-blueprints/tree/document-blueprint-tree.store.ts
#	src/Umbraco.Web.UI.Client/src/packages/documents/document-types/tree/document-type.tree.store.ts
#	src/Umbraco.Web.UI.Client/src/packages/documents/documents/recycle-bin/tree/data/document-recycle-bin-tree.store.ts
#	src/Umbraco.Web.UI.Client/src/packages/documents/documents/tree/document-tree.store.ts
#	src/Umbraco.Web.UI.Client/src/packages/media/media-types/tree/media-type-tree.store.ts
#	src/Umbraco.Web.UI.Client/src/packages/media/media/recycle-bin/tree/media-recycle-bin-tree.store.ts
#	src/Umbraco.Web.UI.Client/src/packages/media/media/tree/media-tree.store.ts
#	src/Umbraco.Web.UI.Client/src/packages/members/member-type/tree/member-type-tree.store.ts
#	src/Umbraco.Web.UI.Client/src/packages/static-file/tree/static-file-tree.store.ts
#	src/Umbraco.Web.UI.Client/src/packages/templating/partial-views/tree/partial-view-tree.store.ts
#	src/Umbraco.Web.UI.Client/src/packages/templating/scripts/tree/script-tree.store.ts
#	src/Umbraco.Web.UI.Client/src/packages/templating/stylesheets/tree/stylesheet-tree.store.ts
#	src/Umbraco.Web.UI.Client/src/packages/templating/templates/tree/template-tree.store.ts
2026-04-15 17:07:49 +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
Laura NetoandGitHub 4638406fc9 Tests: Fix unit test build (#22453)
Fix ElementPickerValueConverterTests build by passing IPropertyRenderingContextAccessor

The PublishedProperty constructor was updated to take an
IPropertyRenderingContextAccessor as its 4th argument, but
ElementPickerValueConverterTests was not updated, breaking the
Umbraco.Tests.UnitTests build.
2026-04-13 18:54:50 +02:00
Laura Neto 704ee94101 Merge branch 'main' into v18/dev 2026-04-13 17:40:29 +02:00
6c96ad1f93 Elements: Cleanup element TODOs in infrastructure (#22443)
* Remove obsolete TODO (already fixed to the extend possible)

* Remove irrelevant TODO

* Refactor publishable entity building from DTOs

* Fix TODO for presentation factory

* Move shared view models from Document to Content

* Clarify TODO after testing refactoring feasibility

* Update src/Umbraco.Cms.Api.Management/ViewModels/Content/ScheduleRequestModel.cs

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

* Update src/Umbraco.Cms.Api.Management/Factories/IElementPresentationFactory.cs

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

---------

Co-authored-by: Andy Butland <abutland73@gmail.com>
2026-04-13 08:09:13 +02:00
Kenn JacobsenandGitHub 8cebbd23d8 Elements: Cleanup element TODOs in core (#22399)
* Cleanup element TODOs in core (first take)

* Cleanup more element TODOs in PublishableContentServiceBase and ElementEditingService

* Implement Delivery API for ElementPickerValueConverter (removes TODOs and add a few new ones)

* Move the generic implementation of PublishedElementWrapped to its own class file

* Review comments for IPublishableContentRepository
2026-04-13 08:08:39 +02:00
4a4437b500 Rendering: Use explicit dependency instead of access-via-casting (#22442)
* Use explicit dependency instead of access-via-casting

* Update src/Umbraco.PublishedCache.HybridCache/PublishedElement.cs

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

---------

Co-authored-by: Andy Butland <abutland73@gmail.com>
2026-04-13 07:21:08 +02:00
Andy Butland bd83df28bc Merge branch 'main' into v18/dev 2026-04-11 11:37:15 +02:00
010ceab910 Elements: Align ElementPermissionService for performance improvements (#22405)
* Align ElementPermissionService with ContentPermissionService performance improvements

* Don't fetch entities we don't need.

* Update src/Umbraco.Core/Persistence/Repositories/IEntityRepository.cs

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

* Add unit tests for ElementPermissionService

---------

Co-authored-by: Andy Butland <abutland73@gmail.com>
2026-04-10 12:14:25 +00:00
Andy Butland c6f1cfdd4a Fixed test helpers build. 2026-04-09 21:12:25 +02:00
Andy Butland e09cf2aaf8 Fix build of integration tests and failing unit test. 2026-04-09 19:52:12 +02:00
Andy Butland 85680bd36d Merge branch 'v18/dev' of https://github.com/umbraco/Umbraco-CMS into v18/dev 2026-04-09 16:01:22 +02:00
Andy Butland 36f3bf6b9c Merge branch 'main' into v18/dev 2026-04-09 16:00:10 +02:00
8df1c3f152 Deprecations: Client-side removal of v18 deprecated code (#21984)
* chore(tree): remove deprecated tree store infrastructure

Remove the entire tree store pattern that was deprecated in favor of
direct tree repository queries. This deletes 29 tree store files,
removes the ManifestTreeStore extension type, updates all 15+ tree
repository constructors to remove store context token parameters,
cleans up manifests/constants/index exports, and migrates all
skip/take pagination to the paging property pattern.

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

* chore(workspace): remove deprecated methods and properties

Remove deprecated methods/properties across workspace contexts, menu
structures, tree items, and collections:
- Tree item context: getManifest(), loadMore()
- Content workspace: loadSegments()
- Entity detail workspace: parentUnique/parentEntityType observables,
  getParent/setParent/getParentUnique/getParentEntityType methods,
  _scaffoldProcessData (replaced by _processIncomingData)
- Menu structure contexts: #parent state, provideContext('UmbMenuStructureWorkspaceContext')
- Document/media/blueprint/member workspaces: contentTypeHasCollection,
  getCollectionAlias(), getContentTypeId() (replaced by getContentTypeUnique())
- Collection context: setManifest(), getManifest() from interface and implementation
- Bulk delete action: deprecated _items getter/setter

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

* chore(core): remove deprecated type aliases and exports

Remove deprecated type aliases scheduled for v18 removal:
- PackageManifestResponse (use UmbPackageManifestResponse)
- UmbSectionDefaultElement (use UmbDefaultSectionElement)
- ConditionsCollectionView (use UmbConditionsCollectionView)
- MediaValueType (use UmbMediaValueType)
- UrlParametersRecord (use UmbUrlParametersRecord)
- ActiveVariant (use UmbActiveVariant)
- UmbPropertyValueChangeEvent class and deprecated property-value-change
  event listeners

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

* chore(ui): remove deprecated config and UI exports

- Textarea: remove deprecated minHeight/maxHeight config reads
- Image cropper modal: remove deprecated default export
- UFM filters: remove 3 deprecated camelCase filter manifests
  (StripHtmlCamelCase, TitleCaseCamelCase, WordLimitCamelCase)

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

* chore(repository): make totalAfter/totalBefore mandatory in UmbTargetPagedModel

Make totalAfter and totalBefore required properties (were optional),
fulfilling the TODO to make these mandatory in v18. All downstream
tree data sources already provide these values.

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

* style: fix lint formatting

Auto-fixed formatting from lint run (line wrapping, trailing newlines).

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

* fix(collection): default filter parameter in element collection repositories

The UmbCollectionRepository interface defines filter as optional.
Without a default, calling requestCollection() without arguments
would throw when accessing filter.skip/filter.take.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

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

* fix(backoffice): resolve ESLint errors, fix pagination metadata, and remove missed deprecations

- Remove unused UmbObjectState import (ESLint error from merge)
- Remove unused offsetPaging variable in tree-item-children.manager.ts
- Fix totalBefore/totalAfter in all tree data sources to account for skip
  offset (was always reporting totalBefore: 0 regardless of skip value)
- Remove deprecated entityType property from UmbElementValueModel
  (marked for v18 removal)
- Remove deprecated _items getter/setter from UmbTrashEntityBulkAction
  (marked for v18 removal)

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

* fix(backoffice): remove entityType references from tests and source after type removal

Remove entityType property from test fixtures and media-dropzone.manager.ts
following removal of the deprecated entityType from UmbElementValueModel.

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

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com>
2026-04-09 13:53:48 +00:00
ee44dbd677 Global Elements: Create options with allowed types and entityCreateOptionAction extensions (#22265)
* entity-action manifests shuffle

* feat(elements): show allowed element types in create action modal

Replace the generic document type picker with a custom create options
modal that fetches allowed element types from the library API and
displays them alongside a folder creation option, following the
established Media create pattern.

* feat(elements): add collection create action with allowed types and folder option

Add custom collection action element that fetches allowed element types
and discovers entityCreateOptionAction extensions (e.g. folder creation),
rendering them as a button or dropdown in the collection toolbar.

* refactor(elements): use dynamic entityCreateOptionAction extensions in create modals

Replace hardcoded folder option in the element create options modal with
UmbExtensionsApiInitializer to dynamically discover entityCreateOptionAction
extensions, enabling 3rd party extensibility.

* style(elements): clean up redundant state, magic strings, and empty styles

Use UMB_ELEMENT_ROOT_ENTITY_TYPE constant instead of magic string,
remove unused _headline state and empty css template, inline
single-use getter.

* fix(elements): address PR review feedback and export missing constants

- Extend UmbNamedEntityModel instead of duplicating name field
- Add getHref() support and error handling matching core patterns
- Add max-height on scroll container, icon fallbacks, element-specific
  localization key
- Export UMB_ELEMENT_CREATE_OPTIONS_MODAL and
  UMB_ELEMENT_TYPE_STRUCTURE_REPOSITORY_ALIAS through index chain
- Add feature parity checklist to clean-code docs

* style(elements): add noElementTypes localization entry and lint tweaks

* fix(elements): handle href navigation and error handling in create options modal

Navigate via history.pushState when href is present on create option
actions. Only close modal on successful execute, keeping it open on
failure so users can retry.

* Add temporary .skip tag to element smoke tests due to UI changes - to be fixed in another PR

---------

Co-authored-by: Nhu Dinh <150406148+nhudinh0309@users.noreply.github.com>
Co-authored-by: Nhu Dinh <hnd@umbraco.dk>
2026-04-09 12:40:06 +01:00
MoleandGitHub 05112b559f Management API: Fix ambiguous constructor in PasswordConfigurationPresentationFactory (#22391)
* Fix ambiguous constructor

* Add clarifying comment
2026-04-09 10:09:33 +00:00
0fa669db94 Code Clean-up (18): Remove obsoleted code flagged for removal (Part 3) (#22335)
* remove obsolete code from services

* remove obsolete code from IEmailSenderClient

* remove obsolete code from Notifications

* unchange MemberServiceTest

* Remove obsolete code from CopyingNotification

* remove ContentFinderByUrl and ContentFinderByUrlAndTemplate

* Remove DefaultUrlProvider, remove obsolete code from ContentPermissions, update MemberRoleStoreTests

* unchange PropertyCacheLevelTests

* unchange ConvertersTests

* Update src/Umbraco.Core/Notifications/ContentCopiedNotification.cs

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

* Update src/Umbraco.Core/Notifications/ContentCopyingNotification.cs

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

* Update src/Umbraco.Core/Notifications/CopiedNotification.cs

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

* Update src/Umbraco.Core/Notifications/CopyingNotification.cs

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

* Update src/Umbraco.Core/Notifications/ElementCopiedNotification.cs

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

* Update src/Umbraco.Core/Notifications/ElementCopyingNotification.cs

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

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

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

* Update src/Umbraco.Infrastructure/Mail/BasicSmtpEmailSenderClient.cs

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

* update tests, rename, remove file tests...

* Minor formatting tidy-up.

---------

Co-authored-by: Lan Nguyen Thuy <lnt@umbraco.dk>
Co-authored-by: Andy Butland <abutland73@gmail.com>
2026-04-09 06:57:00 +00:00
e5d44cd9c3 Code Clean-up (18): Resolve V18 TODO comments (#22357)
* todo cleanup

* adding activatorUtilitiesConstructor atribute

* fix failed test by adding ActivatorUtilitiesConstructor

* Apply suggestion from @AndyButland

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

* Apply suggestion from @AndyButland

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

* Apply suggestion from @AndyButland

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

* Apply suggestion from @AndyButland

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

* Apply suggestion from @AndyButland

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

* update umbracoPlan and remove ConfigureSecurityStampOptions

* Removed uneeded using.

---------

Co-authored-by: Lan Nguyen Thuy <lnt@umbraco.dk>
Co-authored-by: Andy Butland <abutland73@gmail.com>
2026-04-09 06:34:42 +00:00
5d682f69a8 UUI: Updates to UI Library version 2.0.0-alpha.1 (#21994)
* build(deps): bumps @umbraco-ui to 2.0.0-alpha.1 with new themes

* fix: updates paths to new themes

* feat: uses new uui themes for static cshtml files

* feat: updates to use UUISelectOption and UUIFormControlWithBasicsMixin

* build: copy all themes to "themes" folder

* build(uui): updates themes path so it works relatively with fonts

* build: updates minimum node.js version to build from 22 to 24 to support UUI

* fix: corrects paths to theme css

* docs: update CLAUDE.md files to reflect UUI 2.x for CMS v18

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

* docs(storybook): adds theme switcher

* docs(storybook): updates paths

* docs(web): document UUI theme CSS pipeline across build files

Add comments linking the files involved in UUI theme CSS handling:
- manifests.ts: where theme CSS paths are declared, with note on UUI origin
- external/uui/vite.config.ts: where themes are copied for production builds
- vite.config.ts: where themes are copied for dev server and PR previews
- copy-to-cms.js: clarifies UUI themes are already in dist-cms at this point

Each file points to the others, making the dependency on UUI theme
filenames visible without adding abstraction.

https://claude.ai/code/session_015ntS4GXa4s9BQHsjvigDh2

* Update package.json

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

* fix: adjusts types

* update lockfile

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Niels Lyngsø <nsl@umbraco.dk>
2026-04-08 10:36:23 +00:00
Nhu DinhandGitHub df3724c830 E2E: QA Remove @smoke tags from element tests (temporary) (#22363)
Temporary remove .smoke tags for the element-related tests
2026-04-08 09:29:06 +07:00
e093ca5f49 Global Elements: Workspace UI updates: split view, variant selector, save modal, and pending changes (#21897)
* feat(elements): add contentTypeIcon observable and _handleSave override to workspace context

Adds contentTypeIcon observable, icon field to UmbElementDetailModel, and maps icon from server response. Adds _handleSave override to remap validation error colors to warning colors during save, matching Document workspace behavior.

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

* feat(elements): add loading state, variant selector, and cleanup to split view

Adds loading state observation and binding, variant selector slot with new element-specific variant selector component, and element sortVariants utility. Removes dead #breadcrumbs CSS rule and reorders splitViewIndex to match Document workspace conventions.

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

* feat(elements): wire up publishing workspace context in variant selector

Consumes UMB_ELEMENT_PUBLISHING_WORKSPACE_CONTEXT in the element variant selector, mirroring the Document pattern. Fixes PUBLISHED_PENDING_CHANGES localization to use the correct key.

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

* feat(elements): add save modal for element workspace variant picker

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

* Adds "Update" permission condition on Folder Rename entity-action

* feat(elements): add pending changes manager for element workspace

Mirror the Document workspace's UmbDocumentPublishedPendingChangesManager
to provide client-side comparison of persisted vs published element data.
The variant selector now uses this manager to determine pending changes
state instead of relying solely on the API state. The actual API call to
fetch published element data is left as a TODO until the backend endpoint
exists.

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

* Update src/Umbraco.Web.UI.Client/src/packages/elements/modals/save-modal/element-save-modal.element.ts

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

* Update src/Umbraco.Web.UI.Client/src/packages/elements/utils.ts

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

* refactor(menu): delegate breadcrumb href to menu structure context

Move the href resolution logic from the breadcrumb element into the
menu structure workspace context via a new `getItemHref` method on the
interface and base class. This eliminates the need for duplicate
breadcrumb elements that only differ in href behavior, and mirrors the
existing pattern used by the variant breadcrumb.

* feat(elements): add menu structure context and breadcrumb for element folders

Add UmbElementFolderMenuStructureContext that overrides getItemHref to
make folder ancestors and the section root clickable in the breadcrumb.
Register the menu structure context and breadcrumb footer app in the
element folder workspace manifests.

* fix(elements): provide synthetic variant data for folder tree items

Folders don't have variants from the API, so provide a synthetic
published variant using the folder name. This prevents errors when
the tree item mapper expects variant data.

* Updates "umb-element-table-collection-view"

to add the column elements for "name" and (published) "state".

* Refactor exports in constants.ts for clarity

* fix(workspace): prevent breadcrumb TypeError for contexts without getItemHref

Menu structure contexts that don't extend the tree base class (e.g.
UmbLanguageNavigationStructureWorkspaceContext) lack getItemHref, causing
a runtime TypeError in the breadcrumb element. Use optional chaining to
gracefully handle missing implementations.

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

* docs(menu): add JSDoc to UmbMenuStructureWorkspaceContext interface

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

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Niels Lyngsø <niels.lyngso@gmail.com>
Co-authored-by: Niels Lyngsø <nsl@umbraco.dk>
Co-authored-by: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com>
2026-04-07 11:17:41 +02:00
b972d1db5a Global Elements: Element Tree Item "Draft" state (#22228)
* Adds "umb-element-tree-item" custom component

Updates context to use the item data resolver..

* Adds manifests for Element entity-signs

for "Has Pending Changes" and "Has Scheduled Publish"

* Update src/Umbraco.Web.UI.Client/src/packages/elements/tree/element-tree-item.element.ts

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

* Attempt to fix the Item Data Resolver `setData` type-casting

* Align element tree item model with item model for type safety

Add required `flags` field to `UmbElementTreeItemModel` (via
`UmbEntityWithFlags`) and `UmbElementTreeItemVariantModel`, matching
the document tree pattern. This ensures the data resolver's `#setFlags()`
receives actual data instead of silently accessing undefined properties.

The `as unknown as` cast in the context remains due to nominal type
differences (entityType union, variant state enum) but is now structurally
safe at runtime.

* Maps `flags` in `UmbElementTreeItemVariantModel`

* Updated locator for element tree item due to UI changes

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Niels Lyngsø <nsl@umbraco.dk>
Co-authored-by: Nhu Dinh <hnd@umbraco.dk>
2026-04-03 07:45:12 +00:00
Andy Butland 015d17db3f Merge branch 'main' into v18/dev 2026-04-02 08:19:35 +02:00
Andy Butland 617f2441fc Merge branch 'main' into v18/dev 2026-04-01 15:39:23 +02:00
Niels Lyngsø c18f28d22d Merge branch 'main' into v18/dev
# Conflicts:
#	src/Umbraco.Cms.Persistence.Sqlite/Services/SqliteSyntaxProvider.cs
#	src/Umbraco.Core/Services/OperationStatus/UserOperationStatus.cs
2026-04-01 13:58:53 +02:00
Andy Butland f550eeec28 Fixed client-side build. 2026-04-01 12:36:20 +02:00
Andy Butland 22907e01ef Updated OpenApi.json. 2026-04-01 09:54:54 +02:00
59a7d99e3c Elements: Add PublishedCultures and UnpublishedCultures to ElementCacheRefresher (#22302)
* Add PublishedCultures and UnpublishedCultures to ElementCacheRefresher.JsonPayload

Adds culture-specific publishing details to the element cache refresher payload,
matching the existing ContentCacheRefresher.JsonPayload structure. Also replicates
the performance optimization from #21415 by only clearing partial view cache when
there are actual publish/unpublish culture changes, and fixes the Remove change
type check to use HasType instead of equality (flags enum).

* Reuse content cache logic for partial view cache clearing

---------

Co-authored-by: kjac <kja@umbraco.dk>
2026-04-01 09:27:33 +02:00
Niels Lyngsø f4b3a0f4ac update management api types 2026-04-01 09:19:47 +02:00
Niels Lyngsø 68d9e02417 Merge branch 'main' into v18/dev
# Conflicts:
#	src/Umbraco.Cms.Api.Management/OpenApi.json
#	src/Umbraco.Web.UI.Client/src/packages/core/backend-api/sdk.gen.ts
2026-04-01 09:13:55 +02:00
6e85871595 Global Elements: Recycle Bin UI (#21872)
* Uncommented placeholders for restore endpoints

* Delete (inside Recycle Bin): wired up correct endpoints

* Added condition for "Empty Recycle Bin" collection-action

to only display in the Recycle Bin root.

* feat(recycle-bin): add destination entity overrides to restoreFromRecycleBin kind

Add optional destinationItemRepositoryAlias, destinationItemDataResolver,
and destinationRootEntityType properties to support cross-entity-type
restore (e.g. element restoring into element-folder). Existing document
and media manifests are unaffected as all new properties fall back to
the original values. Also adds element folder restore manifest.

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

* refactor(recycle-bin): extract #resolveDestinationItemName to reduce complexity

Extract resolver logic from setDestination into a dedicated method to
bring cyclomatic complexity under the threshold of 9.

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

* Removed Restore Element Folder From Recycle Bin Entity Action

(This is for a separate PR)

* feat(elements): enable element and folder restore from recycle bin

Uncomment element restore manifest with destination overrides, add
folder picker modal, and add null guard for restore item lookup.

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

* Fixes bug with selecting the Root for the restore target

* Corrected manifest aliases to use appropriate entity-type name for `ElementFolder`

* Added `UmbElementFolderItemDataResolver` to resolve folder names in recycle bin restore modal

* E2E: QA Added acceptance tests for restoring elements and deleting elements from recycle bin (#22069)

* Updated test helper for move a folder to recycle bin

* Added tests for restore element and delete element from recycle bin

* Added ocmment for the failing tests

* Make recycle bin tests run in the pipeline

* Fixed comment

* Removed duplication code

* Reverted npm command

* Adds `itemDataResolver` to the Element Trash entity-action

* Makes trashed Element Folder name to be read-only

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Niels Lyngsø <niels.lyngso@gmail.com>
Co-authored-by: Niels Lyngsø <nsl@umbraco.dk>
Co-authored-by: Nhu Dinh <150406148+nhudinh0309@users.noreply.github.com>
Co-authored-by: Laura Neto <12862535+lauraneto@users.noreply.github.com>
2026-04-01 09:01:05 +02:00
Andy ButlandandGitHub 6853f2c910 EF Core: Align casing of EF Core code constructs (closes #22247) (#22313)
* Align casing of EFCore code constructs.

* Handle code review feedback.
2026-04-01 06:56:48 +02:00
Andy Butland 9d94b26cc2 Merge branch 'main' into v18/dev 2026-03-31 12:12:57 +02:00
58cbc9790f Global Elements: Adds "Start Node" and "Ignore User Start Nodes" to Element Picker configuration (#22255)
* Element Picker property-editor: adds "Start Node" configuration

* [WIP] Adds server config for Element start node

* [WIP] Attempts to wire up the `dataTypeId`

for the Element Picker start node

* Removed `StartNodeId` from the server config

* Implemented `requestTreeStartNode`

on Element Picker data-source

* Fix duplicate config entries in input-element property setters

The `folderOnly` and `startNode` setters used `.push()` without
deduplication, causing config entries to accumulate on Lit re-renders.
Filter existing entries before pushing to prevent duplicates.

* Update OpenAPI spec and regenerate TypeScript bindings

Add dataTypeId query parameter to element tree endpoints.

* Refactor input-element to compute dataSourceConfig on demand

Replace mutable #dataSourceConfig array with plain Lit properties for
folderOnly and startNode, computing the config inline in render. This
eliminates the duplicate-entry bug and simplifies the component.

Also fix "dont" typo in ignoreUserStartNodes description.

---------

Co-authored-by: Nhu Dinh <150406148+nhudinh0309@users.noreply.github.com>
2026-03-31 08:11:06 +01:00
8311bae2ab User Permissions: Resolve and persist element start node IDs when updating a user (#22297)
* Resolve and persist element start node IDs when updating a user

The UpdateAsync method in UserService only resolved Document and Media
start node keys to IDs, completely ignoring ElementStartNodeKeys from
the update model. This caused element start node configuration to be
silently lost on user save.

* Add ElementStartNodeNotFound status and fix XML doc for MapUserUpdate

Introduces a dedicated ElementStartNodeNotFound operation status to
distinguish missing element start nodes from missing element items in
other operations, consistent with ContentStartNodeNotFound and
MediaStartNodeNotFound. Also adds the missing XML doc param for
startElementIds on MapUserUpdate.

* Add blank line to re-trigger the build.

---------

Co-authored-by: kjac <kja@umbraco.dk>
2026-03-31 07:26:58 +02:00
2bfb83d6f7 Global Elements: Add "Allowed in library" toggle to Document Type structure view (#21875)
* Added localization keys

* Fixed mock data

* Adds UI for "Allowed in library" configuration

* Capitalize nouns regarding allow in library

* Focuses `allowedInLibrary` on Document/Element Types

* refactor(web): extract route setup from UmbDocumentTypeWorkspaceContext constructor

Move route configuration into a private #setupRoutes() method to reduce
constructor cyclomatic complexity below the threshold of 9.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

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

---------

Co-authored-by: Niels Lyngsø <nsl@umbraco.dk>
Co-authored-by: Claude <noreply@anthropic.com>
2026-03-30 17:46:13 +01:00
Laura NetoandGitHub f1a7c6bbc1 Localization: Remove unused recycleBin keys from XML language files (#22299)
Remove unused recycleBin keys from XML language files

The recycleBin area contained keys (contentTrashed, mediaTrashed,
elementTrashed, elementContainerTrashed, itemCannotBeRestored,
itemCannotBeRestoredHelpText, wasRestored) that are no longer
referenced by any backend code since the audit logging was removed
from RelateOnTrashNotificationHandler in #21481.
2026-03-30 17:04:34 +02:00
4d993a6dd1 Elements: Add flag support for pending changes and scheduled publish (#21877)
* Add flag support for pending changes and scheduled publish

Add entity sign manifests, tree item rendering, and flag provider
support so element tree items display pending changes (pencil) and
scheduled publish (clock) icons, mirroring the existing document
behavior.

Refactor flag providers and presentation factories to reduce
duplication, and move shared IHasFlags implementation into
PublishableVariantResponseModelBase.

* Fix HasScheduleFlagProvider test mocks to match refactored per-item lookups

* Extract PublishableVariantItemResponseModelBase to deduplicate variant item models

* Extract shared base class from Document/Element presentation factories

Introduce PublishableContentPresentationFactoryBase to eliminate code
duplication between DocumentPresentationFactory and ElementPresentationFactory.
Add async alternatives (CreateVariantsItemResponseModelsAsync,
CreateItemResponseModelAsync, PopulateFlagsAsync) and migrate callers in
async contexts to use them. Sync callers in tree/recycle bin controllers
use .GetAwaiter().GetResult() to avoid breaking changes in base classes.

Add IPublishableContentEntitySlim overload to DocumentVariantStateHelper
to unify the identical IDocumentEntitySlim/IElementEntitySlim overloads.

Make RelationTypePresentationFactory properly async with Task.WhenAll.

* Fix flags fallback to use empty array instead of empty string

* Acceptance Tests: Fix element tree item locator to match both elements and folders

The element tree renders umb-element-tree-item for elements but
umb-default-tree-item for folders. Update the E2E test helper locator
to use :is() to match both custom element types.

* Split HasScheduleFlagProvider into document and element providers

Address PR review feedback:
- Split HasScheduleFlagProvider into HasDocumentScheduleFlagProvider and
  HasElementScheduleFlagProvider with a shared HasScheduleFlagProviderBase
- Fix N+1 query: use batch GetContentSchedulesByKeys instead of per-item
  GetContentScheduleByContentId
- Add GetContentSchedulesByKeys to IPublishableContentService and implement
  in PublishableContentServiceBase, removing the duplicate from IContentService
  and ContentService
- Inject TimeProvider into base class, replacing DateTime.Now with
  _timeProvider.GetUtcNow()
- Split tests to match new provider structure and verify batch retrieval

* Make tree and recycle bin mapping methods async

Remove .GetAwaiter().GetResult() calls introduced by the element flag
support changes. Rename MapTreeItemViewModel to MapTreeItemViewModelAsync
and MapRecycleBinViewModel to MapRecycleBinViewModelAsync across all
tree and recycle bin controllers, properly awaiting async factory calls.

* Extract Task.WhenAll select expressions into named variables

* Add missing XML docs to async methods on IDocumentPresentationFactory

* Fix DateTime vs DateTimeOffset comparison in schedule flag provider

Compare schedule.Date against _timeProvider.GetUtcNow().UtcDateTime
instead of the DateTimeOffset directly, avoiding implicit conversion
issues with DateTimeKind.Unspecified.

---------

Co-authored-by: Andy Butland <abutland73@gmail.com>
Co-authored-by: Niels Lyngsø <nsl@umbraco.dk>
2026-03-30 12:54:37 +02:00
Andy Butland 950470aade Make long-running concurrent save test more robust. 2026-03-28 17:01:11 +01:00
7f1255b5e7 Content Types: Granular content type change types (#22223)
* Add more granularity to ContentTypeChangeTypes and handle for structucal changes (pending non-structucal changes).

* Integration tests to validate the granular, structucal change types

* Implement "other" changes

* Make "other" changes less granular.

* Update tests/Umbraco.Tests.Integration/Umbraco.Core/Services/ContentTypeEditingServiceTests.ChangeTypes.cs

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

* Clean up

* Add test proving the sub-flags do not collide

* Support change detection for both structural and non-structural changes in one operation

---------

Co-authored-by: Andy Butland <abutland73@gmail.com>
2026-03-27 09:55:35 +01:00
6e27ab2e0a Elements: Add missing notifications to element container and element editing services (#22012)
Add missing notifications to element container and element editing services

Add ElementDeletingNotification and ElementTreeChangeNotification to
ElementContainerService for EmptyRecycleBin, Move, MoveToRecycleBin,
and Delete operations, aligning with ContentService notification patterns.

Add ElementTreeChangeNotification to ElementEditingService for Move
and Copy operations.

Refactor DeleteDescendantsLocked to return deleted elements and
DeleteItem to return the deleted entity for use in tree change
notifications.

Co-authored-by: Niels Lyngsø <nsl@umbraco.dk>
2026-03-27 08:55:59 +01:00
Andy Butland da614e7d2c Fixed integration tests failing on SQL Server and NUnit 4. 2026-03-27 08:26:18 +01:00
Andy Butland 858c450223 Merge branch 'main' into v18/dev 2026-03-26 16:51:27 +01:00
e8f5b98cb0 Code Clean-up (18): Remove obsoleted code flagged for removal (Part 2) (#22137)
* Remove obsolete code

* Update tests in BlockEditorBackwardsCompatibilityTests

* update languageId, remove obsolete construcor from ApiLink

* remove the tests

* Fixed build of unit tests.

* Reverted removal of UmbracoApiController for now (we should do this in a single PR).

* Code style fix.

---------

Co-authored-by: Lan Nguyen Thuy <lnt@umbraco.dk>
Co-authored-by: Andy Butland <abutland73@gmail.com>
2026-03-26 15:08:10 +00:00
Nhu DinhandGitHub 339da5e19e E2E: V18 Fixed the failing smoke tests (#22248)
* Reverted fix

* Updated tests regarding duplication due to UI changes
2026-03-25 09:35:40 +00:00
Andy Butland 9ef7c5b955 Further fix to failing acceptance test. 2026-03-25 08:37:35 +01:00
Andy Butland ad8db9c567 Fixed failing integration and acceptance tests after merge. 2026-03-25 06:51:24 +01:00
Andy Butland 0210dd00a0 Fix after merge. 2026-03-24 17:37:17 +01:00
Andy Butland 41f62ae03b Merge branch 'main' into v18/dev 2026-03-24 16:58:46 +01:00
Sven Geusens b19f8a2eb1 Add v18/dev to nightly build trigger 2026-03-24 11:11:51 +01:00
Kenn JacobsenandGitHub 1a86c9f45c Change the default webhook payload type to "minimal" (#22217)
* Change the default webhook payload type to "minimal"

* Include expected defaults in webhook telemetry + use core constants instead of local strings
2026-03-23 09:19:17 +01:00
Nhu DinhandGitHub 5968bae002 Build: Cherry pick #22164 for V18 (#22165)
Serialize E2E stages and stagger branch schedules to reduce agent usage
2026-03-19 21:20:06 +07:00
7014f9a125 Dependencies: Upgrade NUnit and related test dependencies to latest major versions (#22155)
* Update Nunit and AutoFixture.Nunit to new versions

* Adding NonParallelizable

* Add blame-hang timeout to integration tests to detect hanging tests

* remove NonParallelizable, update NUnit3TestAdapter, add Ingore to CoreConfigurationHttpTests

* Resolve CoreConfigurationHttpTests hang with NUnit 4.

  - Use Task.Run in CreateHost to escape NUnit 4's SynchronizationContext
    which deadlocks sync-over-async calls from async test methods.
  - Use await using for factory disposal to avoid same deadlock on shutdown
  - Remove WithWebHostBuilder which wraps the factory in a
    DelegatedWebApplicationFactory that bypasses the CreateHost override.
  - Add ContentRoot property to UmbracoWebApplicationFactory so content
    root can be set without WithWebHostBuilder.
  - Set ModelsBuilder mode to Nothing to prevent BootFailedException.
  - Add AddTestServices for infrastructure test doubles (MainDom, etc.).

* Revert changes to pipelines.

* Remove remaining CollectionAssert using legacy syntax.

---------

Co-authored-by: Lan Nguyen Thuy <lnt@umbraco.dk>
Co-authored-by: Andy Butland <abutland73@gmail.com>
2026-03-19 11:49:08 +00:00
Niels LyngsøandGitHub 7536d6b95f Library: remove library sidebar app (#22139)
remove library sidebar app
2026-03-17 15:09:06 +00:00
Kenn JacobsenandGitHub 6036b13e94 Elements: Clean up container relations before deleting them (#22154)
Clean up element container relations before deleting them
2026-03-17 10:31:38 +01:00
d2e7fc1863 Dependencies: Update selected dependencies to latest major versions (#22060)
* update outdated dependencies to their latest major versions

* change version of JsonPatch.Net back to 3.*.*

* Upgrade Umbraco.Code package

* Update tests

* Resolve NUnit 4 migration issues causing test hangs

* Fix for dotnet test on the pipeline.

* Debug: Fix attempt for integration tests on the pipeline.

* Revert pipeline changes and go back to 5.2.0.

* Debug: Omit suspect tests.

* Debug: Disable tests with timeout.

* Debug: Try 4.6.0.

* Debug: Added reference to Microsoft.CodeAnalysis.CSharp.Workspaces.

* Roll back NUnit upgrade.

---------

Co-authored-by: Lan Nguyen Thuy <lnt@umbraco.dk>
Co-authored-by: Andy Butland <abutland73@gmail.com>
2026-03-17 06:37:21 +01:00
27b7f220a3 Elements: Fixes HasChildren for Element Folder entities (#22142)
* Fixes `HasChildren` for Element Folder entities

* Remove HasChilden mapping

---------

Co-authored-by: kjac <kja@umbraco.dk>
2026-03-16 17:01:08 +00:00
Andreas ZerbstandGitHub 9e360e8dc0 QA: Fix element tree integration tests and SQL Server container service error (#22130)
* fix SQL Server OFFSET/FETCH error

* added ActionElementBrowse.ActionLetter permission
2026-03-16 13:11:23 +07:00
a2acb7a53f Code Clean-up (18): Remove obsoleted code flagged for removal and address TODO comments (#21980)
* remove obsolete constructor

* adjust RootDictionaryTreeController constructor to use non-obsolete constructor and remove obsolete base

* todo action v18

* remove ActivatorUtilitiesConstructor atribute that there's only one constructor

* remove obsolete class and method

* remove obsolete code in v18

* remove obsolete code from repositories

* remove obsolete for blocks

* remove obsolete code from services

* remove Icomponent

* remove incorrect IRequestSegmmentService

* remove obsolete properties

* undo change of blocklayoutitembase because of test failed

* bring back somes code due to pr 21999

* bring back some codes and update ContentRouteBuildertests

* remove obsolete code from domains, notification controller and some services

* remove obsolete constructor from ElementMapDefinition

---------

Co-authored-by: Lan Nguyen Thuy <lnt@umbraco.dk>
Co-authored-by: Niels Lyngsø <nsl@umbraco.dk>
2026-03-13 14:40:13 +01:00
Nhu DinhandGitHub d9db6b02ce E2E: QA Added .skip tags to failing acceptance tests due to known issues (#22122)
* Added .skip tags for the failing tests due to an actual issue

* Change the way to verify the validation message

* Added .skip tags for failing tests due to the actual issues
2026-03-13 16:30:00 +07:00
Nhu DinhandGitHub ae80d921c0 E2E: QA Updated acceptance tests in v18 due to the auth changes (#22110)
Updated tests due to the auth changes
2026-03-13 03:18:13 +00:00
Andy Butland 14a2ca89ea Adds XML documentation to elements management API controllers, models and mappers. 2026-03-12 18:20:13 +01:00
Andy Butland b2517e3302 Fix post merge issues. 2026-03-12 18:05:48 +01:00
Andy Butland f83949c508 Merge branch 'main' into v18/dev 2026-03-12 17:59:17 +01:00
Andy Butland af439c6a66 Fixes and additional documentation after merge. 2026-03-12 16:16:11 +01:00
Andy Butland 71d700ea28 Merge branch 'main' into v18/dev 2026-03-12 16:15:40 +01:00
Niels Lyngsø 90b2062d85 Merge branch 'main' into v18/dev
# Conflicts:
#	src/Umbraco.Cms.Api.Management/Controllers/Content/ContentControllerBase.cs
#	src/Umbraco.Web.UI.Client/package.json
#	src/Umbraco.Web.UI.Client/src/packages/core/backend-api/sdk.gen.ts
#	tests/Umbraco.Tests.AcceptanceTest/lib/helpers/ContentUiHelper.ts
#	tests/Umbraco.Tests.Integration/CompatibilitySuppressions.xml
#	version.json
2026-03-12 14:21:54 +01:00
Jacob Overgaard e6a91e5f6c Merge remote-tracking branch 'origin/main' into v18/dev 2026-03-11 15:30:02 +01:00
Andreas Zerbst 421d616682 Merge branch 'main' into v18/dev
# Conflicts:
#	tests/Umbraco.Tests.AcceptanceTest/lib/helpers/ApiHelpers.ts
#	tests/Umbraco.Tests.AcceptanceTest/lib/helpers/DataTypeUiHelper.ts
#	tests/Umbraco.Tests.AcceptanceTest/lib/helpers/UiBaseLocators.ts
#	tests/Umbraco.Tests.AcceptanceTest/lib/helpers/UserApiHelper.ts
#	tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/DataType/MediaPicker.spec.ts
#	tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Users/Permissions/User/ContentStartNodes.spec.ts
#	tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Users/Permissions/User/MediaStartNodes.spec.ts
#	tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Users/UserGroups.spec.ts
2026-03-10 17:05:11 +01:00
Niels LyngsøandGitHub fb5030010c Form Control: only validate if value was changed during focus (#21815)
* poc of minimizing unrelevant validation messages

* remove submit method from interface

* remove call to re-validate, as that is already trigger via `updated`--callback
2026-03-09 08:44:46 +00:00
Laura NetoandGitHub 421ad35034 Elements: Split content type validation for create and update (#21906)
* Split content type validation for create and update to allow saving elements no longer permitted in library

* Add integration test for element update after AllowedInLibrary toggle

Verify that ElementEditingService.UpdateAsync succeeds when the content
type's AllowedInLibrary flag is set to false after the element was
created, covering the split validation introduced for create vs update.

* Move content type validation into TryGetAndValidateContentType override

Eliminate redundant content type lookups in CreateAsync and UpdateAsync
by moving the IsElement/AllowedInLibrary check into the
TryGetAndValidateContentType override, which distinguishes create from
update by checking if the model is a ContentCreationModelBase.

* Use Assert.Multiple for element property assertions in update test

* Extract IsAllowedLibraryElement static method for readability
2026-03-06 17:34:00 +00:00
Andy Butland a17aef5af8 Fix sortable property update issue introduced in merge from main. 2026-03-06 17:55:07 +01:00
Andy Butland 6544c5cbcc Merge branch 'main' into v18/dev 2026-03-06 11:05:17 +01:00
Andy Butland a0518a0636 Merge branch 'main' into v18/dev 2026-03-06 08:09:37 +01:00
Andy Butland 5e669bb8c7 Merge branch 'main' into v18/dev 2026-03-06 08:08:28 +01:00
Nhu DinhandGitHub 29ef442e99 E2E: QA Added acceptance tests for element picker in content and element (#21745)
* Added tests for content with element picker

* Added tests for element with element picker

* Bumped version

* Renamed tests

* Make tests run in the pipeline

* Bumped version

* Fixed failing tests

* Moved goToBackOffice step to beforeEach

* Moved goToBackOffice to beforeEach

* Fixed comment

* Fixed afterEach() step

* Fixed import

* Fixed

* Reverted npm command
2026-03-06 10:51:52 +07:00
2ae5582a9e Recycle Bin: Adds destination overrides to restoreFromRecycleBin entity-action kind (#21867)
* feat(recycle-bin): add destination entity overrides to restoreFromRecycleBin kind

Add optional destinationItemRepositoryAlias, destinationItemDataResolver,
and destinationRootEntityType properties to support cross-entity-type
restore (e.g. element restoring into element-folder). Existing document
and media manifests are unaffected as all new properties fall back to
the original values. Also adds element folder restore manifest.

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

* refactor(recycle-bin): extract #resolveDestinationItemName to reduce complexity

Extract resolver logic from setDestination into a dedicated method to
bring cyclomatic complexity under the threshold of 9.

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

* Removed Restore Element Folder From Recycle Bin Entity Action

(This is for a separate PR)

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 13:45:59 +01:00
0ec334e252 fix(media): allow focal point to be set to null in image cropper (#21340)
* fix(media): allow focal point to be set to null in image cropper

- Updated UmbImageCropperPropertyEditorValue type to allow null for focalPoint
- Changed component state to use null as default instead of { left: 0.5, top: 0.5 }
- Replaced logical OR (||) with nullish coalescing (??) to preserve null values
- Updated reset function to set focalPoint to null
- Added null handling in all rendering and calculation logic
- Components now default to center (0.5, 0.5) for display when focalPoint is null

Fixes #21273

* refactor(media): extract logic from initializeCrop to reduce function size

- Extracted mask dimension calculation into #calculateMaskDimensions
- Extracted mask style application into #applyMaskStyles
- Extracted image scale calculation into #calculateImageScales
- Extracted image position calculation into separate methods:
  - #calculateImagePositionWithCoordinates (for existing crops)
  - #calculateImagePositionWithFocalPoint (for focal point positioning)
- Extracted image style application into #applyImageStyles
- Extracted zoom level update into #updateZoomLevel

Reduces #initializeCrop from 72 lines to 33 lines, meeting CI/CD threshold of 70 lines.

Related to #21273

* refactor(media): replace primitive parameters with interfaces to fix code quality warnings

- Created ViewportDimensions interface to group viewport width/height
- Created MaskDimensions interface to group mask dimensions and position
- Created ImageDimensions interface to group image dimensions and position
- Refactored all functions to use interface objects instead of multiple primitives
- Reduced #calculateImageDimensionsAndPosition from 5 args to 2
- Reduced #calculateImagePositionWithCoordinates from 5 args to 2
- Reduced #calculateImagePositionWithFocalPoint from 4 args to 1

Fixes primitive obsession (85.7% -> reduced) and excessive function arguments warnings.

Related to #21273

* fix(media): update modal value interface to allow null focal point

- Updated UmbImageCropperEditorModalValue interface to allow null for focalPoint
- Added explicit null handling when assigning focalPoint in onChange handler

Fixes TypeScript build error where null focalPoint was not assignable to non-nullable type.

Related to #21273

* Refactor image cropper focal-point handling

* Set defaultFocalPoint to null in test file.

* Default focalPoint to null and adjust checks.

---------

Co-authored-by: Francluob <francluob.dev@gmail.com>
Co-authored-by: Engiber Lozada <89547469+engijlr@users.noreply.github.com>
Co-authored-by: Emma L Garland <1649855+emmagarland@users.noreply.github.com>
Co-authored-by: engjlr <enl@umbraco.dk>
2026-03-05 10:57:50 +00:00
Niels Lyngsø db9cbcb457 Merge branch 'main' into v18/dev 2026-03-05 11:07:26 +01:00
Niels Lyngsø d04f8d9029 fit unit test types 2026-03-05 11:04:41 +01:00
Andy Butland c4d09893cd Merge branch 'main' into v18/dev 2026-03-05 09:22:54 +01:00
Jacob Overgaard 040735b1c1 build: optimises azure static builds in order not to consume too many environments 2026-03-05 08:32:25 +01:00
Andy Butland d92502b212 Merge branch 'main' into v18/dev 2026-03-04 12:15:01 +01:00
Andy Butland 6b00925a6a Merge branch 'main' into v18/dev 2026-03-04 11:55:57 +01:00
Andy Butland b9142ad728 Merge branch 'main' into v18/dev 2026-03-04 11:54:49 +01:00
Nhu DinhandGitHub b71c2e53b7 E2E: QA Added acceptance tests for reference tracking info tab of elements (#21949)
* Added tests for element reference tracking in info tab

* Removed tags

* Make all ElementReferenceTracking tests run in the pipeline

* Moved goToBackOffice step to beforeEach

* Updated import file

* Make tests run in the pipeline before merging

* Fixed npm command

* Revert npm command
2026-03-04 10:11:46 +00:00
598a2186d7 Elements: Treat local elements as global elements (#21795)
* Make local and global elements behave the same (use the same implementation)

* Await async calls, don't fire-and-forget

* Fix the remaining unit tests

* Flush static fields on friendly published extensions before starting tests

---------

Co-authored-by: Laura Neto <12862535+lauraneto@users.noreply.github.com>
2026-03-04 10:05:52 +01:00
fcdfb9e588 E2E: QA: Merge moved testhelpers/builders from 17 to 18 (#21970)
* Moved helpers/builder from v18

* Updated existing helpers/builder

* Updated ui helper for updating property editor in document type

* Fixed failing tests

* Revert changes to package-lock

* Cherry pick latest updates from main

---------

Co-authored-by: Nhu Dinh <hnd@umbraco.dk>
2026-03-04 12:02:24 +07:00
Jacob Overgaard b00840e147 build(login): syncs lockfile 2026-03-03 16:20:43 +01:00
Jacob Overgaard f2473f7f74 build(login): syncs lockfile 2026-03-03 16:19:08 +01:00
Jacob Overgaard 673d97c19b build(login): syncs package files 2026-03-03 16:17:06 +01:00
Andy Butland 32acc62cde Merge branch 'main' into v18/dev 2026-03-03 15:44:45 +01:00
Laura Neto c568db2704 Re-generate Umbraco.Tests.AcceptanceTest/package-lock.json 2026-03-03 12:45:34 +01:00
Andy Butland fa6c5d0537 Merge branch 'main' into v18/dev 2026-03-03 11:44:10 +01:00
Laura NetoandGitHub 62d9a002f7 Elements: Add missing documentation endpoint attributes (#21979)
Add missing EndpointSummary and EndpointDescription attributes to element recycle bin restore controllers
2026-03-03 08:47:01 +01:00
Andy Butland 8b59e8eb3b Merge branch 'main' into v18/dev 2026-03-03 08:06:31 +01:00
Niels Lyngsø eec6a27cca Merge branch 'main' into v18/dev
# Conflicts:
#	tests/Umbraco.Tests.AcceptanceTest/package-lock.json
#	tests/Umbraco.Tests.AcceptanceTest/package.json
#	tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Users/Permissions/User/ContentStartNodes.spec.ts
#	tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Users/Permissions/User/MediaStartNodes.spec.ts
2026-03-02 14:00:47 +01:00
Nhu DinhandGitHub 3b68fef220 E2E: QA Added acceptance tests for global elements (#21608) 2026-02-26 17:28:21 +00:00
Andy Butland 92ec78086d Merge branch 'main' into v18/dev 2026-02-26 07:06:34 +01:00
cf9c2908b4 Elements: Add permission-based filtering to element tree endpoints (#21729)
* Add permission-based filtering to element tree endpoints

The element tree endpoints now filter results based on the current
user's browse permissions via a new IElementPermissionFilterService,
mirroring the existing document tree behavior.

Also extracts shared filtering logic from DocumentPermissionFilterService
into a PermissionFilterServiceBase to avoid duplication.

* Add unit tests for ElementPermissionFilterService

* Replace document-specific inheritdoc with neutral XML docs in PermissionFilterServiceBase

* Fix GetPermissionsAsync to use the provided objectTypes parameter instead of hardcoded Document type

---------

Co-authored-by: Kenn Jacobsen <kja@umbraco.dk>
2026-02-25 15:13:42 +00:00
Laura NetoandGitHub 765a3b2968 Tests: Set AllowedInLibrary on element content type in permission tests (#21908)
Set AllowedInLibrary on element content type in permission tests

The GetElementPermissionsCurrentUserControllerTests were failing because the
test setup created an element content type without setting AllowedInLibrary
to true. The ElementEditingService.TryGetAndValidateContentType method now
requires both IsElement and AllowedInLibrary to be true for element creation.
2026-02-25 15:02:12 +01:00
Laura NetoandGitHub 89c7bb356b Elements: Replace block keys on element copy and save (#21814)
* Handle element saving and copying notifications in complex property editors

Extend ComplexPropertyEditorContentNotificationHandler to also handle
ElementSavingNotification and ElementCopyingNotification, ensuring that
block property key replacement (BlockList, BlockGrid, RichText) is
applied to elements the same way it is for content.

* Add integration tests for element copy with block editors

Test that block keys are regenerated and block structure is preserved
when copying elements with BlockList, BlockGrid, and RichText editors,
for both invariant and culture-variant content.
2026-02-25 14:06:55 +01:00
Andy Butland 023c08fcdb Merge branch 'main' into v18/dev 2026-02-25 11:30:01 +01:00
Andreas ZerbstandGitHub 29233a3455 QA: E2E: Added v18/dev so it runs on the nightly test pipeline (#21902)
Removed v15dev and added 18dev to nightly pipeline
2026-02-25 10:22:04 +00:00
Andy Butland 7b97bdd0ef Merge branch 'main' into v18/dev 2026-02-25 09:41:15 +01:00
Andy Butland 21597fa2f8 Merge branch 'main' into v18/dev 2026-02-24 06:57:14 +01:00
Andy Butland 336039f963 Merge branch 'main' into v18/dev 2026-02-24 06:40:18 +01:00
Laura NetoandGitHub c7125967c9 Elements: Add scheduled publishing support for elements (#21796)
* Add scheduled publishing support for elements

Move PerformScheduledPublish from IContentService to the shared
IPublishableContentService<T> interface so both documents and elements
support scheduled publishing.

Filter ClearSchedule and HasContentForRelease/Expiration queries in
PublishableContentRepositoryBase by NodeObjectTypeId to prevent document
and element schedules from interfering with each other.

Update ScheduledPublishingJob to process both document and element
schedules, and add integration tests verifying cross-entity isolation.

* Simplify ScheduledPublishingJob.ExecuteAsync

Extract duplicated scheduled publishing logic into a generic helper
method and include the entity type in the log message.
2026-02-23 20:12:00 +01:00
3e0a3ff1ea Content Version Cleanup: Include element versions in the background cleanup job (#21839)
* Add element version cleanup to the content version cleanup background job

The existing ContentVersionCleanupJob only cleaned up document versions.
Element versions were left to accumulate despite having the same cleanup
service infrastructure available. This extends the job to also clean up
element versions using the same configuration toggle and schedule.

* Use PascalCase for structured logging names.

* Fixed code warnings and duplicate line breaks.

* Cleaned up usings.

---------

Co-authored-by: Andy Butland <abutland73@gmail.com>
2026-02-23 18:58:15 +00:00
Laura NetoandGitHub fa95f956a0 Elements: Add audit log retrieval endpoint and UI support (#21777)
* Add missing AuditType.Copy audit log for element copy operations

Make the abstract Copy method in ContentEditingServiceBase async and
accept a Guid userKey instead of int userId, allowing the element
copy implementation to use the audit service directly. Add the
missing _auditService.AddAsync(AuditType.Copy, ...) call in
ElementEditingService.CopyAsync to match the document equivalent
in ContentService.Copy.

* Fix copy audit log to record against the original element

The copy audit entry was being logged against the new copy's ID
instead of the original element's ID, inconsistent with how
documents handle copy audit logging.

* Add audit log retrieval endpoint for elements and wire up frontend

Add GET /{id:guid}/audit-log endpoint following the document audit
log pattern. Wire up the existing frontend data source to call the
new API, add element-specific localization strings, and remove the
unsupported sort audit type.
2026-02-23 08:43:36 +01:00
Niels Lyngsø ba83cc1006 Merge branch 'main' into v18/dev 2026-02-20 13:48:48 +01:00
Laura Neto 17dd0757d3 Merge branch 'main' into v18/dev 2026-02-20 13:38:06 +01:00
Laura NetoandGitHub e02a3d452c Repository Caches: Fix GUID cache key prefix in PublishableContentRepositoryBase (#21836)
Fix GUID repository cache key prefix in PublishableContentRepositoryBase

The merge of the GUID cache key collision fix (9ea0520) applied
IContent-specific changes from DocumentRepository's nested class, but
in v18/dev this code lives in the generic base class. Two issues:

- EntityByGuidReadRepository.GetCacheKey used the "uRepo_" prefix
  while GuidReadRepositoryCachePolicy looks up entries with "uRepoGuid_",
  causing PopulateCacheByKey to insert under a key the policy never finds.
- PersistUpdatedItem cleared GetGuidKey<IContent> instead of
  GetGuidKey<TEntity>, so ElementRepository would clear the wrong key.
2026-02-20 11:35:52 +00:00
Niels Lyngsø 33a30c38dc Merge branch 'main' into v18/dev 2026-02-19 10:50:40 +01:00
Andy Butland 9a0309ac62 Merge branch 'main' into v18/dev 2026-02-19 10:03:10 +01:00
Niels Lyngsø 31ffc9ff02 Merge branch 'main' into v18/dev 2026-02-17 10:37:21 +01:00
8e911d728d Elements: Add AllowedInLibrary flag to content types with dedicated endpoint (#21723)
* Add AllowedInLibrary flag to content types

Add a new boolean property AllowedInLibrary across all layers to
indicate whether a content type is allowed in the library. This is
only meaningful for element types (IsElement = true).

Changes span the core domain model, Management API request/response
models, persistence DTOs/mappers/factories, and a database migration
to add the column to the cmsContentType table.

* Enforce AllowedInLibrary in ElementEditingService.CreateAsync

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

* feat(api): add AllowedInLibrary filter to document type search endpoint

Add allowedInLibrary query parameter to GET /document-type/search,
following the same pattern as the existing isElement filter. The old
SearchAsync overload without the parameter is preserved as a default
interface method and marked obsolete (scheduled for removal in v19).

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

* chore(api): regenerate OpenApi.json and backoffice client types

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

* feat(api): make search query parameter nullable for document type search

Allow the document type search endpoint to be called without a text
query, enabling filter-only usage (e.g. filtering by isElement and
allowedInLibrary without requiring a search term).

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

* feat(api): replace search allowedInLibrary filter with dedicated endpoint

Revert the search endpoint changes (IContentTypeSearchService, controller)
and instead add a dedicated GET /document-type/allowed-in-library endpoint
that follows the AllowedAtRoot pattern. This ensures IContentTypeFilter
support and a cleaner API separation.

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

* test(api): add integration tests for GetAllAllowedInLibraryAsync and AllowedInLibraryDocumentTypeController

Add service-level tests verifying correct filtering by IsElement + AllowedInLibrary, pagination, and IContentTypeFilter integration. Add controller-level authorization tests for the allowed-in-library endpoint.

* Map allowedInLibrary through content type data sources

Add allowedInLibrary to UmbContentTypeModel and map it consistently
across document, media, and member type data sources for scaffold, read,
create, and update operations.

* Add AllowedInLibrary support to test builders and set it in all element tests

ElementEditingService.CreateAsync checks contentType.AllowedInLibrary and
returns NotAllowed if false. All element test types were missing this flag,
causing test failures. Adds IWithAllowedInLibraryBuilder interface, extension
method, and sets AllowedInLibrary=true on all element type creation in tests.

* Also enforce IsElement check when creating elements in the library

* Set IsElement and AllowedInLibrary on ElementPublishingServiceTests content types

* Remove AllowedInLibrary from document type tree item response model

The AllowedInLibrary property is not relevant for tree items and is not
used by the frontend. This removes it from the tree item model, its
mapping in the tree controller, and regenerates the OpenAPI spec and
TypeScript client accordingly.

* Refactor element content type validation into base class override

Make TryGetAndValidateContentType protected virtual in
ContentEditingServiceBase and override it in ElementEditingService to
check IsElement and AllowedInLibrary. This guards both create and update
paths (previously only create was guarded) and eliminates duplicate
ContentTypeNotFound handling.

Enable the previously-ignored
Cannot_Create_Element_Based_On_NonElement_ContentType test and add a new
test for the AllowedInLibrary check.

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 10:01:34 +01:00
Andy Butland 7a12f056e4 Merge branch 'main' into v18/dev 2026-02-17 07:33:01 +01:00
Kenn JacobsenandGitHub 391e8a0867 Fix the integration tests project file structure (#21766) 2026-02-16 11:27:11 +00:00
0ca1a861e9 Elements: Add webhooks support (#21697)
* Add webhooks for elements

* Review: Removed unused payload type

* Use new object as empty payload

---------

Co-authored-by: Laura Neto <12862535+lauraneto@users.noreply.github.com>
2026-02-16 10:36:00 +01:00
Laura Neto e3135685da Merge branch 'main' into v18/dev
# Conflicts:
#	src/Umbraco.Core/Services/UserService.cs
2026-02-16 10:28:54 +01:00
6f0fd8f6ec Elements: Add reference settings support (#21601)
* Elements: Add DisableDeleteWhenReferenced support and fix delete notifications

- Add DisableDeleteWhenReferenced check to ElementContainerService delete operations
- Fire ElementDeletedNotification and EntityContainerDeletedNotification per item during descendant deletion
- Fix potential infinite loop when items are skipped due to being referenced
- Simplify EmptyRecycleBinAsync to use DeleteDescendantsLocked directly
- Use path descending ordering for consistent deletion order (children before parents)
- Add test for descendant delete notifications

* Elements: Fix EmptyRecycleBin pagination with DisableDeleteWhenReferenced

When DisableDeleteWhenReferenced is enabled and some items are skipped,
the standard skip/take pagination breaks. This change:

- Adds SqlLessThan/SqlGreaterThan SQL expression extensions for string
  comparison in LINQ queries
- Uses path-based cursor pagination instead of skip/take
- Tracks protected paths to prevent deleting containers that have
  referenced descendants
- Adds ElementRecycleBin to UmbracoObjectTypes enum

* Tests: Add DisableUnpublishWhenReferenced tests for elements

Verify that DisableUnpublishWhenReferenced works correctly for elements
(inherited from ContentPublishingServiceBase):
- Cannot unpublish an element that is being referenced
- Can unpublish an element that is doing the referencing

* Elements: Remove redundant Trashed filter from DeleteDescendantsLocked

The Trashed filter was redundant because:
- EmptyRecycleBinAsync only operates on items under the recycle bin root
- DeleteFromRecycleBinAsync requires containers to be trashed, and all
  descendants are marked as trashed when moved to recycle bin

Removing the filter simplifies the query and handles edge cases better.

* Elements: Add proper ProblemDetails responses for publish/unpublish endpoints

Move ContentPublishingOperationStatusResult from DocumentControllerBase to
ContentControllerBase so it can be shared. Add ElementPublishingOperationStatusResult
to ElementControllerBase and update PublishElementController and
UnpublishElementController to return proper error responses instead of empty
BadRequest() when operations fail (e.g., when DisableUnpublishWhenReferenced is enabled).

* Refactor: Use abstract EntityName for content controller error messages

Replace hardcoded "document" terminology in shared ContentControllerBase
error messages with an abstract EntityName property, so each subclass
(document, element, media, member, etc.) provides context-appropriate
error messages.

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

* Fix: Check DisableUnpublishWhenReferenced when moving elements to recycle bin

ElementEditingService.MoveToRecycleBinAsync was missing the reference
check that ContentEditingService already performs for documents. This
allowed referenced elements to be moved to the recycle bin even when
DisableUnpublishWhenReferenced was enabled.

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

* Prevent moving container to recycle bin when descendants are referenced

Add server-side validation to ElementContainerService.MoveToRecycleBinAsync
that checks for referenced descendants when DisableUnpublishWhenReferenced
is enabled. Uses ITrackedReferencesService.GetPagedDescendantsInReferencesAsync
as an upfront check before any move processing begins.

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-13 16:02:43 +01:00
Laura NetoandGitHub ebcb996431 Elements: Fix delete blocked by trash-tracking relation (#21725)
Fix element delete blocked by trash-tracking relation

ElementEditingService was missing the RelateParentOnDeleteAlias
override, so the "relate parent on delete" relation created when
trashing was not excluded from the reference check. This caused
"Cannot delete a referenced content item" when
DisableDeleteWhenReferenced was enabled, even for unreferenced
elements.
2026-02-13 07:30:28 +01:00
8cbf68223a Global Elements: UI refinements, element picker and constants tidy-up (#21737)
* improvement(elements): general UI updates, element picker rework, and constants tidy-up

* fix(elements): forward min/max messages through umb-input-element and minor cleanups

Add minMessage/maxMessage properties to UmbInputElementElement so validation
messages are properly forwarded to the inner umb-input-entity-data component.
Also fix JSDoc grammar, variable naming, and comment tidying.

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

* fix(elements): sync value/selection and register inner form control in umb-input-element

Add getter/setter overrides for value and selection that keep them in sync
(matching umb-input-content pattern), and register the inner umb-input-entity-data
via addFormControlElement() in firstUpdated() so validation propagates correctly.

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

* fix(elements): use correct element ID in referenced-by mock handler

Change sentinel ID from 'all-property-editors-document-id' to 'simple-element-id'
to match the actual element mock fixture IDs in element.data.ts.

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

* test(elements): add unit test for umb-input-element

Add instantiation and conditional a11y audit tests following the
umb-input-document.test.ts pattern.

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

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 18:49:56 +00:00
39f19bf3ab Global Elements: Rollback UI (#21712)
* feat(elements): add element rollback repository, modal, and audit log

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

* Exported rollback constants

* Update src/Umbraco.Web.UI.Client/src/packages/elements/rollback/modal/rollback-modal.element.ts

Co-authored-by: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com>
2026-02-12 17:08:08 +00:00
Niels Lyngsø e8c0dd897b Merge branch 'main' into v18/dev 2026-02-12 16:27:28 +01:00
66fbade194 Global Elements: Workspace Validation UI (#21711)
* feat(elements): add element workspace validation repository

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

* Global Elements: Address Copilot review feedback on validation PR

Fix JSDoc comments on validation repository/data-source to accurately
describe validation behavior instead of persistence. Use barrel import
for validation repository and remove leftover commented-out code.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

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

* Global Elements: Remove redundant guard clauses in validation data source

Remove TypeScript-redundant checks in validateCreate to reduce
cyclomatic complexity below the CodeScene threshold of 9.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

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

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com>
2026-02-12 15:12:23 +01:00
ae2761f204 Global Elements: Reference Tracking UI (#21710)
* feat(elements): add element reference tracking repository

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

* Fixed linting errors

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 15:02:20 +01:00
Andy Butland a49981b6c5 Merge branch 'main' into v18/dev 2026-02-12 06:50:15 +01:00
Lee KelleherandGitHub d21d1453f5 Global Elements: Exports all constants for @umbraco-cms/backoffice/element (#21727) 2026-02-11 17:52:40 +01:00
Niels Lyngsø 11e19466f8 Merge branch 'main' into v18/dev 2026-02-11 12:52:14 +01:00
Laura Neto d51de53804 chore(api): regenerate OpenApi.json and backoffice client SDK
The OpenAPI definition and backoffice TypeScript client were out of
sync with recent Management API changes already on v18/dev. Regenerated
to bring them up to date.
2026-02-10 15:25:21 +01:00
Laura Neto 8b5448adcd Merge branch 'main' into v18/dev
# Conflicts:
#	src/Umbraco.Core/Constants-Security.cs
#	src/Umbraco.Core/Extensions/ClaimsIdentityExtensions.cs
#	src/Umbraco.Core/Models/PublishedContent/PublishedContentBase.cs
#	src/Umbraco.Infrastructure/Events/RelateOnTrashNotificationHandler.cs
#	src/Umbraco.PublishedCache.HybridCache/PublishedContent.cs
#	src/Umbraco.Web.UI.Client/src/packages/core/backend-api/sdk.gen.ts
2026-02-10 14:02:51 +01:00
cf70d7ff13 Global Elements: Refactor content and element repositories into a common base (#21637)
* Begin implementation of repo base

* Move internal mapping - part 1

* Move internal mapping - part 2

* Move versioning, persistence, GUID sub repo and utilities to base

* Fix wrong assumption in cache tests

* Move content repo + recycle bin to base

* Move schedule to base

* Move common delete clauses to base

* Move DTO mapping to base

* Fix a few of the pending TODOs for elements

* Abstract OnUowRefreshedEntity away to concrete implementations

* Handle template editing in a less hardcoded way

* Restore DTO visibility for elements

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

Co-authored-by: Laura Neto <12862535+lauraneto@users.noreply.github.com>

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

Co-authored-by: Laura Neto <12862535+lauraneto@users.noreply.github.com>

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

Co-authored-by: Laura Neto <12862535+lauraneto@users.noreply.github.com>

* Update cache key (review comment)

---------

Co-authored-by: Laura Neto <12862535+lauraneto@users.noreply.github.com>
2026-02-10 09:54:38 +01:00
f0dc972bf2 Elements: Add restore from recycle bin functionality (#21556)
* Elements: Add restore from recycle bin functionality

- Add RestoreAsync to IElementEditingService and ElementEditingService
- Add RestoreAsync to IElementContainerService and ElementContainerService
- Add RestoreElementRecycleBinController and RestoreElementFolderRecycleBinController API endpoints
- Add TryGetContainedObjectType to EntityContainer for graceful handling of non-container types
- Update EntityContainerRepository to return null instead of throwing for non-container entities
- Add comprehensive integration tests for element and container restore operations

* Refactor: Remove entity return from Move/Restore/MoveToRecycleBin methods

Simplify the return types of IElementEditingService and IElementContainerService
move operations to return only the operation status instead of the entity.

These operations don't meaningfully change entity data (just location/state),
and no consumers were using the returned entities. Callers can use GetAsync
if they need the updated entity afterward.

* Fix: Capture original path before move for restore relation cleanup

The MoveEventInfo.OriginalPath was incorrectly set to the element's path
after the move, causing DeleteOriginalParentRelationsOnRestore to fail
because the path no longer contained the recycle bin path prefix.

* Tiny little formatting

---------

Co-authored-by: kjac <kja@umbraco.dk>
2026-02-09 13:39:25 +00:00
Andy Butland 81276fc048 Adds endpoint summaries and descriptions to new controllers introduced since 17. 2026-02-09 13:04:54 +01:00
Andy Butland 58f300a20d Merge branch 'main' into v18/dev 2026-02-09 12:36:23 +01:00
Niels Lyngsø f530772b80 Merge branch 'main' into v18/dev 2026-02-05 12:49:26 +01:00
Niels Lyngsø 8cea6cb6b5 Merge branch 'main' into v18/dev 2026-02-05 09:06:55 +01:00
Laura NetoandGitHub 7cf4c7857f Elements: Add reference tracking and recycle bin query support (#21481)
* Elements: Add reference tracking and recycle bin query support

- Add Element reference tracking API endpoints (referenced-by, are-referenced, referenced-descendants)
- Add Element recycle bin original-parent and referenced-by endpoints
- Add ElementReferenceResponseModel and ElementContainerReferenceResponseModel
- Add IElementRecycleBinQueryService for querying original parents of trashed elements
- Add Element relation type constants for parent tracking on delete
- Add translation strings for Element recycle bin operations
- Refactor RelateOnTrashNotificationHandler to reduce code duplication using generic helper methods
- Add Element and ElementContainer support to RelateOnTrashNotificationHandler

* Elements: Register Element notification handlers for relation tracking

Add Element and EntityContainer notification handlers for:
- RelateOnTrashNotificationHandler (move to/from recycle bin)
- ContentRelationsUpdate (track element content relations)

* Elements: Remove ReferencedDescendantsElementController

Elements are leaf nodes in the folder structure and cannot have
descendants, making this endpoint unnecessary.

* Elements: Fix ElementPickerPropertyEditor reference extraction

The element picker stores element IDs as Guids, not as UDI strings.
Updated GetReferences to deserialize as Guid array and create UDIs
from the Guid values.

* Elements: Fix TrackedReferencesRepository to include Element published state

Add LEFT JOIN to ElementDto and use COALESCE to get the published state
from either DocumentDto or ElementDto, fixing the issue where Element
references returned published = null.

* Elements: Add ReferencedDescendantsElementFolderController

Add endpoint to get referenced descendants of an element folder.
Unlike elements (which are leaf nodes), folders can have descendants
that may be referenced elsewhere.

* Elements: Add integration tests for Element reference tracking

Add TrackedReferencesServiceElementTests covering:
- GetPagedRelationsForItemAsync for Elements
- GetPagedRelationsForRecycleBinAsync for Elements
- GetPagedKeysWithDependentReferencesAsync for Elements
- GetPagedDescendantsInReferencesAsync for Element containers

* Elements: Add management API controller permission tests for Element reference endpoints

- Add ReferencedByElementControllerTests for element referenced-by endpoint permissions
- Add AreReferencedElementControllerTests for element are-referenced endpoint permissions
- Add ReferencedDescendantsElementFolderControllerTests for folder descendants endpoint permissions
- Fix GetManagementApiUrl to respect [FromQuery(Name="...")] attribute for proper URL generation

* Elements: Split OriginalParentElementRecycleBinController into two controllers

Split the controller to follow the controller-per-operation pattern,
consistent with DeleteElementRecycleBinController and
DeleteElementFolderRecycleBinController.

- OriginalParentElementRecycleBinController: for elements
- OriginalParentElementFolderRecycleBinController: for folders

* Elements: Fix user fallback for audit logging in RelateOnTrashNotificationHandler

Update the handler to properly resolve user key for audit logging, using a switch expression
to handle different entity types. Also sets CreatorId on EntityContainer creation.

* Tests: Fix duplicate query parameter in ItemElementItemControllerTests

Remove the ClientRequest() override that was appending a duplicate id query
parameter to the URL. The base MethodSelector already includes the element key
which gets converted to the query parameter by GetManagementApiUrl, causing
the URL to become ?id=<guid>?id=<guid> and model binding to fail.

* Tests: Fix permission controller tests to use correct entity types

These tests were passing on the base branch only because the URL was being
constructed incorrectly (missing query parameters). After be399134f8 fixed
the GetManagementApiUrl helper to properly include FromQuery parameters,
the tests now correctly build URLs and revealed that they were using user
keys instead of the expected document/media/element node keys.

Updated tests to create the appropriate entity type (document, media, or
element) and pass its key to the permission endpoints.

* Tests: Update RelationTypeRepositoryTest for new element relation types

Update expected counts and fix hardcoded ID lookup after new element
reference tracking relation types were added to the system:
- umbElement (RelatedElement)
- relateParentElementContainerOnElementDelete
- relateParentElementContainerOnContainerDelete

Changes:
- Store created test relation type in field to use actual ID instead of
  hardcoded ID 9 which shifted when built-in types were added
- Update GetAll expected count from 9 to 12 (9 built-in + 3 test data)
- Update Count query expected from 6 to 8 (aliases starting with "relate")

* Refactor: Rename methods in RelateOnTrashNotificationHandler for clarity

Rename methods and parameters to better describe their purpose:
- DeleteRelationsOnRestore → DeleteOriginalParentRelationsOnRestore
- CreateRelationsOnTrashAsync → CreateOriginalParentRelationOnTrashAsync
- relationTypeAlias → originalParentRelationTypeAlias
- relationTypeName → originalParentRelationTypeName

These names clarify that the methods handle "original parent" relations
used for restoring items from the recycle bin, not all relations.

* Tests: Fix ReferencedDescendantsElementFolderControllerTests expectations

- Use unique folder names to prevent conflicts between test runs
- Add assertion to verify folder creation succeeds
- Correct expected status codes for Editor and Writer to OK (not NotFound)

The NotFound responses were caused by folder creation failures due to
duplicate names, not actual permission restrictions.

* Tests: Add success assertions to Element controller test setup methods

Add Assert.IsTrue checks after service calls in test setup to ensure
test prerequisites are correctly established before running actual tests.
This prevents silent failures in setup from causing misleading test results.

Assertions added for:
- ElementContainerService.CreateAsync (9 tests)
- ElementEditingService.CreateAsync (19 tests)
- ElementEditingService.MoveToRecycleBinAsync (6 tests)
- ElementContainerService.MoveToRecycleBinAsync (3 tests)

* Elements: Fix MapReference to return un-enriched response when entity not found

Return the mapped response model instead of null when the matching entity
cannot be found for enrichment. This preserves basic reference information
even when variant data cannot be loaded, preventing valid references from
being silently dropped.

Also clean up ElementContainerReferenceResponseModel formatting.

* Fix: Guard GetSlimEntities against empty keys to prevent loading all entities

* Fix: Return ParentIsTrashed status when original parent is in recycle bin

* Breaking: Remove duplicate sync notification handler interfaces

Remove INotificationHandler<ContentMovedToRecycleBinNotification> and
INotificationHandler<MediaMovedToRecycleBinNotification> interfaces along
with their obsolete sync Handle methods. Only the async handlers should
be implemented.

* Tests: Simplify TrackedReferencesServiceElementTests

- Simplify assertions in Get_Descendants_In_References test
- Create Element3 after folder creation to avoid unnecessary update

* Revert: Remove changes to be moved to separate PRs

Revert EntityTypeContainerService.CreateAsync CreatorId change and
permission controller test changes - these should be addressed in
separate PRs.

* Revert: Remove GetMediaPermissionsCurrentUserControllerTests changes

This change should be addressed in a separate PR for v17.

* Refactor: Move recycle bin audit logging to services

Move audit logging for recycle bin operations from RelateOnTrashNotificationHandler
to the individual services (ContentService, MediaService, ElementEditingService,
ElementContainerService). This simplifies the notification handler and keeps audit
logging closer to the operations being performed.

- Simplify audit messages to "Moved to recycle bin from parent {parentId}"
- Add AuditMoveToRecycleBin helper methods to Content and Media services
- Add AuditMoveAsync helper methods to Element services
- Remove unused audit dependencies from RelateOnTrashNotificationHandler
- Add obsolete constructor bridge for backwards compatibility

* Refactor: Extract GetParentIdFromPath extension method

Add GetParentIdFromPath string extension to consolidate duplicate logic
for extracting parent ID from entity path strings. This replaces 5
instances of the same path parsing pattern across services and handlers.

- Add GetParentIdFromPath to StringExtensions.Parsing.cs
- Inline audit calls in ContentService, MediaService,
  ElementEditingService, and ElementContainerService
- Update RelateOnTrashNotificationHandler to use the new extension
- Add unit tests for the new extension method

* Refactor: Make CreateOriginalParentRelationOnTrash synchronous

Remove unnecessary async from CreateOriginalParentRelationOnTrash since
the method contains no async operations. Update handlers to return
Task.CompletedTask directly.
2026-02-04 14:54:50 +00:00
5fe5a0febf Elements: Implement validation for Element editing endpoints (#21562)
* Elements: Implement validation for Element editing endpoints

Move ValidateCulturesAndPropertiesAsync and GetCulturesToValidate from
ContentEditingService to ContentEditingServiceBase, enabling reuse in
ElementEditingService.

- Implement ValidateCreateAsync and ValidateUpdateAsync in ElementEditingService
- Update Element API controllers to return validation results properly
- Update all inheriting services (Media, Member, Blueprint) with new params

* Elements: Add validation tests for ElementEditingService

- Add tests for ValidateUpdateAsync and ValidateCreateAsync
- Cover invariant, culture variant, and permission-based validation scenarios

* Fix bad merge

* Removed unused fields

* Removed old editor UI

---------

Co-authored-by: kjac <kja@umbraco.dk>
2026-02-04 13:35:54 +00:00
Andy Butland d4ee843a01 Merge branch 'main' into v18/dev 2026-02-04 12:06:42 +01:00
Niels Lyngsø c48ef57a84 Merge branch 'main' into v18/dev 2026-02-04 09:20:08 +01:00
Andy Butland 0c3d1d7058 Merge branch 'main' into v18/dev 2026-02-03 17:14:16 +01:00
Andy Butland eda9857200 Merge branch 'main' into v18/dev 2026-02-03 09:37:26 +01:00
46b96f3811 Global Elements - take one (#21431)
* CRUD + folders + API

* Fix infinite recursion

* Distributed cache handling for Elements

* Publishing for Elements (incl. refactor)

* Fix bad file name

* Added "foldersOnly" option to the siblings endpoint

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

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

* API for publishing elements

* Published element cache (WIP)

* Fix delete at repo level

* Fixing up a little tests

* Element picker property editor

* Added tests to prove published element status

* Move scheduled content keys to base abstraction

* Add request caching for published element creation (similar to published document creation)

* Apply conditional appcache access to elements as well

* Fix test build errors

* Fix merge from main

* Fix merge

* Add cache invalidation on update (like content and media)

* Move element (incl. tests)

* Element copying

* Add items endpoint incl. variation info at item level

* Make the Element tree items look like Document tree items (with variations)

* Rename all things ElementType to DocumentType

* Move ElementRepository to the right place

* Fix auditing after merge (changes from #19357)

* Fix dates after merge (changes from #19822)

* Fix NPoco querying after merge (changes from #20184)

* Fix various build errors after merge

* Move containers

* Add migration to create element tables

* Re-implement #21105 at base class level

* Fix merge

* Add element tree recycle bin + move element to/from recycle bin

* Controllers for move to recycle bin + recycle bin root

* Support element containers in recycle bin (no controllers)

* Handle error cases for element moves and add more tests

* Do not allow creation of IPublishedElement for trashed elements

* Amend recycle bin controller output and add children controller

* Regenerate OpenApi.json with Element APIs

* Housekeeping: Organize element container service tests

* Fix bad housekeeping

* Add missing siblings controller for recycle bin

* Add "delete from recycle bin" and "empty recycle bin" operations (including API)

* Updated OpenApi.json to reflect new endpoints

* Added `CreateDate` to `ElementTreeItemResponseModel`

Marked `ElementRecycleBinItemResponseModel.DocumentTypeReferenceResponseModel` as nullable.

* Re-generated OpenAPI.json

* Add configuration endpoint for Elements

* Explicitly unpublish published elements when restoring from recycle bin.

* Elements: Remove invalid templateId from ElementVersionDto index definitions (#21384)

* Persistence: Remove invalid templateId from ElementVersionDto index definitions

The ElementVersionDto had index definitions that referenced templateId in
their IncludeColumns, but the ElementVersion table only has id and published
columns. This caused SQL Server clean installs to fail with error 1911:
"Column name 'templateId' does not exist in the target table or view."

This was likely a copy-paste error from DocumentVersionDto which does have
a templateId column.

* Ignore Cannot_Create_Element_Based_On_NonElement_ContentType for the time being

---------

Co-authored-by: kjac <kja@umbraco.dk>

* Fix the ordering of items in the tree

* It's 2026 now...

* Fix missing project structure

* Amend empty recycle bin

* Elements: Fix element recycle bin node insertion on SQL Server (#21390)

Enable IDENTITY_INSERT before inserting the element recycle bin node with an explicit ID, then disable it afterward. This fixes the migration failing on SQL Server with "Cannot insert explicit value for identity column" error.

* Fix count trashed children

* Moved newly added entity service tests to an isolated, per-test DB class so they do not interfere with the existing per-fixture DB tests

* Elements: Element start node permissions (#21375)

* Add Element start node support for Users and UserGroups

- Add StartElementId to UserGroup and element start nodes to User
- Add UserStartNodeFolderTreeControllerBase for tree filtering with folder support
- Update ElementTreeControllerBase to use start node filtering
- Add ElementTreeItemResponseModel.NoAccess property for "no access" items
- Add UserExtensions methods for element start node calculation
- Update User/UserGroup API models and factories
- Add database migration for startElementId column
- Add SectionAccessForElementTree authorization policy

Note: Granular element permissions deferred for future implementation

* Add element root access for default user groups on fresh install

Set StartElementId = -1 for Administrators, Writers, Editors, and
Translators user groups in DatabaseDataCreator, giving them element
root access on fresh installations (matching their content/media access).

* Add multi-type support to UserStartNodeEntitiesService

Added overloads to RootUserAccessEntities, ChildUserAccessEntities, and
SiblingUserAccessEntities that accept multiple UmbracoObjectTypes. This
enables querying for Elements and ElementContainers in a single call
rather than requiring separate queries for each type.

Also added GetAll and GetPagedChildren overloads to IEntityService and
IEntityRepository to support querying multiple object types efficiently
with a single database query.

* Add integration tests for Element start nodes with mixed hierarchy

Added UserStartNodeEntitiesServiceElementTests with a mixed hierarchy
structure containing both containers and elements at each level:
- Level 1: Containers (C1-C5) and Elements (E1-E3)
- Level 2: Child containers (C1-C1 through C1-C10) and Elements (C1-E1, C1-E2)
- Level 3: Leaf elements (C1-C1-E1 through C1-C1-E5)

This tests scenarios where containers and elements are siblings, ensuring
the access filtering works correctly for mixed-type queries.

Also refactored Content and Media tests to use a shared base class
(UserStartNodeEntitiesServiceTestsBase) to reduce code duplication.

* Add Library section for Elements

- Rename Constants.Applications.Elements to Library
- Add SectionAccessLibrary authorization policy
- Add library mapping to SectionMapper
- Grant Library section access to Administrators, Writers, and Editors on fresh install
- Update TreeAccessElements to use Library section

* Add Element tree controller authorization tests

Add integration tests for RootElementTreeController and
ChildrenElementTreeController to verify section-based
authorization works correctly for the Element tree endpoints.

* Fix ReadOnlyUserGroup not passing startElementId to constructor

The obsolete 13-parameter constructor was passing `null` instead of
the actual `startElementId` value to the next constructor, causing
user groups to appear to have no element start node access.

Also update UserFactory.ToReadOnlyGroup to pass the Description
parameter to the ReadOnlyUserGroup constructor.

* Add Element controller authorization tests

Add authorization tests for Element CRUD, Folder, RecycleBin, and Item
controllers to verify user group access permissions.

Tests cover Admin, Editor, Writer, SensitiveData, Translator, and
Unauthorized user groups for each controller endpoint.

* Re-generated OpenApi.json

* Fix Element start node handling to use ElementContainer object type

- Update UserStartNodeFolderTreeControllerBase to query both folder and
  item object types when filtering by user start nodes
- Fix UserGroupPresentationFactory to use ElementContainer instead of
  Element when resolving element start node IDs/keys

* Revert ByKeyElementController to use synchronous Task.FromResult

The method doesn't have any async operations, so async/await adds
unnecessary overhead.

* Fix UserPresentationFactory to use ElementContainer for element start nodes

Element start nodes reference ElementContainer (folders), not Element items.

* Add recycle bin start node access test for Element controllers

- Add WithStartElementId to UserGroupBuilder
- Add ElementRecycleBinControllerTestBase with shared test verifying
  users with non-root element start nodes cannot access recycle bin
- Update all Element recycle bin tests to use the new base class

* Fix UserGroupPresentationFactory and Element test section alias

- Use ElementContainer instead of Element for start node lookups in
  IReadOnlyUserGroup overload
- Use Constants.Applications.Library for Element test section alias

* Add obsolete User constructor overload for backward compatibility

- Add obsolete constructor without startElementIds parameter that delegates
  to the new constructor with an empty array
- Improve XML documentation for all User constructors

* Elements: Move NoAccess property to FolderTreeItemResponseModel base class

This allows both elements and folders to indicate access status in the tree.

* Elements: Add API versioning attributes to SiblingsElementTreeController

* Elements: Add integration tests for element tree start node permissions

Add tests to verify that users with element start node restrictions can only see
and access elements within their permitted hierarchy.

* Group test files

* Remove type check from GetAllPaths overload

---------

Co-authored-by: Kenn Jacobsen <kja@umbraco.dk>

* Elements: Add rollback (#21393)

* Services, repos and tests

* Endpoints for Elements versioning

* Add extra test to prove handling of pinned versions

* Renaming from PR review

* Update tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ElementVersionCleanupServiceTest.cs

Co-authored-by: Laura Neto <12862535+lauraneto@users.noreply.github.com>

* More code clean-up after review

* Use correct deleting/deleted versions notifications

---------

Co-authored-by: Laura Neto <12862535+lauraneto@users.noreply.github.com>

* Elements: Regenerate OpenApi.json

* Elements: Add default and granular permissions for Element controllers (#21385)

* Add Element start node support for Users and UserGroups

- Add StartElementId to UserGroup and element start nodes to User
- Add UserStartNodeFolderTreeControllerBase for tree filtering with folder support
- Update ElementTreeControllerBase to use start node filtering
- Add ElementTreeItemResponseModel.NoAccess property for "no access" items
- Add UserExtensions methods for element start node calculation
- Update User/UserGroup API models and factories
- Add database migration for startElementId column
- Add SectionAccessForElementTree authorization policy

Note: Granular element permissions deferred for future implementation

* Add element root access for default user groups on fresh install

Set StartElementId = -1 for Administrators, Writers, Editors, and
Translators user groups in DatabaseDataCreator, giving them element
root access on fresh installations (matching their content/media access).

* Add multi-type support to UserStartNodeEntitiesService

Added overloads to RootUserAccessEntities, ChildUserAccessEntities, and
SiblingUserAccessEntities that accept multiple UmbracoObjectTypes. This
enables querying for Elements and ElementContainers in a single call
rather than requiring separate queries for each type.

Also added GetAll and GetPagedChildren overloads to IEntityService and
IEntityRepository to support querying multiple object types efficiently
with a single database query.

* Add integration tests for Element start nodes with mixed hierarchy

Added UserStartNodeEntitiesServiceElementTests with a mixed hierarchy
structure containing both containers and elements at each level:
- Level 1: Containers (C1-C5) and Elements (E1-E3)
- Level 2: Child containers (C1-C1 through C1-C10) and Elements (C1-E1, C1-E2)
- Level 3: Leaf elements (C1-C1-E1 through C1-C1-E5)

This tests scenarios where containers and elements are siblings, ensuring
the access filtering works correctly for mixed-type queries.

Also refactored Content and Media tests to use a shared base class
(UserStartNodeEntitiesServiceTestsBase) to reduce code duplication.

* Add Library section for Elements

- Rename Constants.Applications.Elements to Library
- Add SectionAccessLibrary authorization policy
- Add library mapping to SectionMapper
- Grant Library section access to Administrators, Writers, and Editors on fresh install
- Update TreeAccessElements to use Library section

* Add Element tree controller authorization tests

Add integration tests for RootElementTreeController and
ChildrenElementTreeController to verify section-based
authorization works correctly for the Element tree endpoints.

* Fix ReadOnlyUserGroup not passing startElementId to constructor

The obsolete 13-parameter constructor was passing `null` instead of
the actual `startElementId` value to the next constructor, causing
user groups to appear to have no element start node access.

Also update UserFactory.ToReadOnlyGroup to pass the Description
parameter to the ReadOnlyUserGroup constructor.

* Add Element controller authorization tests

Add authorization tests for Element CRUD, Folder, RecycleBin, and Item
controllers to verify user group access permissions.

Tests cover Admin, Editor, Writer, SensitiveData, Translator, and
Unauthorized user groups for each controller endpoint.

* Re-generated OpenApi.json

* Fix Element start node handling to use ElementContainer object type

- Update UserStartNodeFolderTreeControllerBase to query both folder and
  item object types when filtering by user start nodes
- Fix UserGroupPresentationFactory to use ElementContainer instead of
  Element when resolving element start node IDs/keys

* Revert ByKeyElementController to use synchronous Task.FromResult

The method doesn't have any async operations, so async/await adds
unnecessary overhead.

* Fix UserPresentationFactory to use ElementContainer for element start nodes

Element start nodes reference ElementContainer (folders), not Element items.

* Add recycle bin start node access test for Element controllers

- Add WithStartElementId to UserGroupBuilder
- Add ElementRecycleBinControllerTestBase with shared test verifying
  users with non-root element start nodes cannot access recycle bin
- Update all Element recycle bin tests to use the new base class

* Fix UserGroupPresentationFactory and Element test section alias

- Use ElementContainer instead of Element for start node lookups in
  IReadOnlyUserGroup overload
- Use Constants.Applications.Library for Element test section alias

* Add obsolete User constructor overload for backward compatibility

- Add obsolete constructor without startElementIds parameter that delegates
  to the new constructor with an empty array
- Improve XML documentation for all User constructors

* Elements: Add granular permissions for Element controllers

Add Element-specific permission actions:
- ActionElementBrowse, ActionElementNew, ActionElementUpdate, ActionElementDelete
- ActionElementPublish, ActionElementUnpublish, ActionElementMove, ActionElementCopy

Add permission infrastructure:
- ElementPermissionResource for authorization checks
- ElementPermissionHandler and ElementPermissionRequirement
- ElementPermissionService and IElementPermissionService
- ElementPermissionAuthorizer and IElementPermissionAuthorizer
- ElementGranularPermission model
- ElementPermissionMapper for user group permissions

Update Element controllers with authorization:
- Add HandleRequest pattern via CreateElementControllerBase and UpdateElementControllerBase
- Pass cultures for Publish/Unpublish authorization
- Apply authorization checks to Element CRUD and publishing operations

* Elements: Add default element permissions to user groups

Add element action permissions for Admin, Editor, Writer, and Translator
user groups in DatabaseDataCreator, mirroring the document permission pattern.

* Elements: Add current user element permissions endpoint and fix folder authorization

- Add GetElementPermissionsCurrentUserController endpoint to get current user's element permissions
- Fix ElementPermissionService to authorize both Element and ElementContainer (folders)
- Add GetElementPermissionsAsync to IUserService/UserService
- Add ElementNodeNotFound to UserOperationStatus
- Add IEntityService.GetAll overloads for multiple object types

* Elements: Move NoAccess property to FolderTreeItemResponseModel base class

This allows both elements and folders to indicate access status in the tree.

* Elements: Add default implementation to IUserService.GetElementPermissionsAsync

Adds a default throwing implementation to avoid breaking existing IUserService implementations when this method is added.

* Elements: Add API versioning attributes to SiblingsElementTreeController

* Elements: Add integration tests for element tree start node permissions

Add tests to verify that users with element start node restrictions can only see
and access elements within their permitted hierarchy.

* Add granular permissions to element rollback

* Update src/Umbraco.Core/Actions/ActionElementCopy.cs

Co-authored-by: Kenn Jacobsen <kja@umbraco.dk>

* Elements: Use lowercase action aliases for consistency

Update all Element action aliases to lowercase to comply with the
IAction.Alias requirement for case-sensitive filesystems. Also rename
ActionElementNew alias from "elementNew" to "elementcreate" to match
the document action's "create" alias pattern.

* Elements: Refactor UserService permission methods to reduce duplication

Consolidate GetMediaPermissionsAsync, GetDocumentPermissionsAsync, and
GetElementPermissionsAsync into a single shared implementation via
a new private GetContentPermissionsAsync helper method.

---------

Co-authored-by: kjac <kja@umbraco.dk>

* Add element folder "item" endpoint

* Include "isTrashed" in folder response models

* Update TODOs

* Rollback a few unnecessarily breaking signature changes

* Use schema constants from #21327

* Elements: Add admin group element permissions during upgrade (#21452)

Grant the admin user group access to the element root node and all
element permissions when upgrading from a previous version. This
ensures parity with fresh installations where the admin group receives
these permissions by default.

* Elements: Fix Writer expected status codes in Element controller permission tests

Update WriterUserGroupAssertionModel to expect Forbidden for operations
that Writers don't have permission for, matching Document controller
behavior and the actual permissions assigned to the Writer group.

Changed from OK/Created to Forbidden:
- CopyElementControllerTests
- DeleteElementControllerTests
- MoveElementControllerTests
- MoveToRecycleBinElementControllerTests
- PublishElementControllerTests
- UnpublishElementControllerTests
- Folder/DeleteElementFolderControllerTests
- Folder/MoveElementFolderControllerTests
- Folder/MoveToRecycleBinElementFolderControllerTests
- RecycleBin/DeleteElementRecycleBinControllerTests
- RecycleBin/DeleteElementFolderRecycleBinControllerTests
- RecycleBin/EmptyElementRecycleBinControllerTests

* Elements: Fix duplicate column name in DocumentVersionDto index definition

The ForColumns parameter incorrectly specified PublishedColumnName twice
instead of IdColumnName and PublishedColumnName, causing SQL Server to
reject index creation with "duplicate column names" error on new installs.

* Add missing element mapper and allow deleting element types with active elements (#21483)

* Add missing element mapper and allow deleting element types with active elements

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

Co-authored-by: Laura Neto <12862535+lauraneto@users.noreply.github.com>

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

Co-authored-by: Laura Neto <12862535+lauraneto@users.noreply.github.com>

---------

Co-authored-by: Laura Neto <12862535+lauraneto@users.noreply.github.com>

* Update src/Umbraco.Core/Cache/Refreshers/Implement/ElementCacheRefresher.cs

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

* Review comment: ReadOnlyUserGroup constructor

* Update comments in ElementEditingService

* Add Library section access to content, media, and member tree policies

* Elements: Add Elements access to data type, document type, and relation authorization policies (#21501)

Add Elements access to data type, document type, and relation authorization policies

* Amend merge from v18/dev

* Global Elements: Backoffice UI implementation (#21410)

* chore: generate new openapi types

* Added package/module for "Library"

* Added default dashboard for Library section

* [WIP] Adds "Elements" package module

Basics of the tree/menu.

* Adds entity-actions for Create and Reload

* Adds entity-action for Move To

* Adds collection workspace view

for root and folders

* Adds entity-action for Duplicate To

* "Reload Children" should only be for root & folders

* Reworked Library sidebar app

Replaced with Elements sidebar app
Removed the Library menu

* chore: generate new openapi types

* Added Item repository

* Added Reference repository

* Added Element Recycle Bin

Tree, menu, entity-actions, workspace (collection view)

* Adds "umb-element-tree-item" to identify the `isTrashed` state

* Re-added Library sidebar app

Removed Library dashboard (we'll figure it out later)

* Recycle Bin type tweaks

* [WIP] Element "Create" modal

* Reverted Element "Create" modal, to use create-options + picker

* chore: generate new openapi types

* Added Element Detail Repository

* [WIP] Element Workspace + Context

* Elements: Add workspace views for edit and info

Add edit and info workspace views to the Element workspace:
- Edit view using shared 'contentEditor' kind pattern
- Info view displaying state tag, dates, element type, and ID
- Menu structure context for tree navigation
- Split-view component for variant editing

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

* Elements: Add save action and trash state handling

- Add Save workspace action using UmbSubmitWorkspaceAction
- Add isTrashed property to UmbElementDetailModel
- Implement trash state change handling with read-only guard
- Add recycle bin event listeners for trash/restore actions

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

* Adds Workspace actions for Save, Publish, Scheduled Publish

* Adds Element Configuration repository

* Adds mock handle + data for Elements

* Adds Publish and Unpublish entity actions for Elements

Implements context menu actions for publishing and unpublishing elements
directly from the tree. Uses existing modals and publishing repository.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

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

* package-lock.json update

* Adds bulk entity actions for Publish, Unpublish, Move and Trash

* Localization keys + code tweaks

* Adds reusable `emptyRecycleBin` `collectionAction` kind

* Adds `emptyRecycleBin` for Element Recycle Bin collection

* Element Recycle Bin refactoring

Working towards folder support

* Relations: exported entity-action types

* Restructured "Element Folder" code

* Restructured "Trash" entity-bulk-action code

* Adds `trashFolder` `entityAction` kind

* Adds "Trash" entity-action for Element Folders

* Tidy-up / restructuring

* [WIP] Element Picker property-editor UI

making use of an Elements property-data-source,
with Entity Picker.

* Renamed `UmbElementPropertyDatasetContext` to `UmbElementWorkspacePropertyDatasetContext`

to de-duplicate a class name clash with the underlying base class.

* Added "entity-data-picker" importmap

Exposing the "umb-input-entity-data" component

* Reworking the "Element Picker" property-editor UI

to reuse the Entity Picker internal input component

* Implemented "Element Item Data Resolver" helper

* chore: generate new openapi types

* Fixed up the mocks and types

with new Element start nodes and `noAccess` fields.

* Added UI for "Elements Start Nodes"

* Added "entity-data-picker" export to the Vite config

* Fixed Element Folder picker for "start nodes"

* Adds UI for Element's User Permissions

* Adds Element User Permission condition

Implemented the user permissions for entity actions, etc.

* Adds UI for Element's Granular Permissions

* Adds element-folder item repository

* Element Recycle Bin: implemented `isTrashed`

* Fixed mock folder data manager

* Adds move entity-action for element-folder

Implements the Move action for element folders using the
ElementService.putElementFolderByIdMove API endpoint.

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

* Fix typos and element tag name mismatches in elements package

- Fix typo 'now' -> 'no' in user-permissions/types.ts
- Fix HTMLElementTagNameMap tag name to match @customElement decorator
- Fix typo 'TDOD' -> 'TODO' in element-detail.server.data-source.ts
- Fix missing 'u' prefix in element-picker tag name declaration

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

* Ignore local Claude settings in UI Client

* Updated workspace assign access,

to disable root access when start nodes are selected.

* Elements: Display trashed state in Element workspace info panel (#21542)

The state tag in the Element workspace info view was missing a case
for the TRASHED state, causing trashed Elements to incorrectly display
"Not created" instead of "Trashed".

* Elements: Fix folder link in recycle bin list view (#21543)

The trashed element name column always used the element workspace path
pattern, causing folders clicked in the recycle bin list view to show
"Not found". Now checks isFolder and uses the correct workspace path
pattern for folders vs elements.

* Elements: Add missing delete permission conditions to recycle bin actions (#21547)

The Empty Recycle Bin collection action and the folder delete entity
action were missing user permission conditions, making them visible
to users without delete permission.

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Co-authored-by: Laura Neto <12862535+lauraneto@users.noreply.github.com>

---------

Co-authored-by: Andy Butland <abutland73@gmail.com>
Co-authored-by: leekelleher <leekelleher@gmail.com>
Co-authored-by: Laura Neto <12862535+lauraneto@users.noreply.github.com>
Co-authored-by: Lee Kelleher <leekelleher@users.noreply.github.com>
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-03 07:46:31 +01:00
Laura Neto 7d73588c99 Merge branch 'main' into v18/dev 2026-01-28 13:27:22 +01:00
kjac 40c91262e6 Merge branch 'main' into v18/dev 2026-01-27 10:32:50 +01:00
Niels Lyngsø 31f23204f3 Merge branch 'main' into v18/dev 2026-01-27 09:19:30 +01:00
e53b8bcc52 Variants Sorting: Sort by language name (fix #21408) (#21435)
* Sort at last by language name

* ensure document language picker is sorted as variant selector

* Update src/Umbraco.Web.UI.Client/src/packages/documents/documents/modals/shared/document-variant-language-picker.element.ts

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

* Update src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-split-view/workspace-split-view-variant-selector.element.ts

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

* Update src/Umbraco.Web.UI.Client/src/packages/documents/documents/utils.ts

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

* refactor to avoid inline methods

* transform into a function

* revert config file commit

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-01-23 09:18:52 +01:00
Niels Lyngsø 25d3013949 Merge branch 'main' into v18/dev 2026-01-16 17:18:05 +01:00
Andy Butland 8ac46e99b7 Applied naming suggestions made in review of #21374. 2026-01-15 07:51:34 +01:00
Andy Butland 59cc153b84 Merge branch 'main' into v18/dev 2026-01-15 07:46:41 +01:00
29ecae7010 Entities: Prevent changing Key property on existing entities (#21374)
* Prevent setting of entity Key to a new value for already persisted entities.

* Handled file based entities that have a key dependent on their path, so need to be able to have the key changed on move.

* Fixed package data update of content type to resolve failing integration test.

---------

Co-authored-by: Kenn Jacobsen <kja@umbraco.dk>
2026-01-14 06:45:11 +00:00
Laura Neto 409f5072af Merge branch 'main' into v18/dev 2026-01-13 10:09:56 +01:00
Andy ButlandandGitHub 58a0b160b2 Umbraco Helper: Align GetDictionaryValue nullability with behaviour (#21372)
Align GetDictionaryValue nullability with behaviour (returns empty string when no dictionary item is found for the provided key).
2026-01-13 06:42:17 +01:00
Andy Butland c8ba79b2d1 Merge branch 'main' into v18/dev 2026-01-12 11:54:52 +01:00
ef1b48c992 Obsolete Code: Remove obsolete methods and constants relating to allowed application and start node claims (#20124)
* Delete GetStartContentNodes

* Delete GetStartMediaNodes

* Delete GetAllowedApplications

* Delete ClaimTypes

* Update protected recycle bin functionality to no longer used removed claim details.  Added unit tests to verify behaviour.

* Fixed failing unit tests now the number of claims included is reduced.
Addressed comments from code review.

* Minor code tidy.

* Expose claim necessary for retrieving the user key.

---------

Co-authored-by: Andy Butland <abutland73@gmail.com>
2026-01-09 12:25:33 +00:00
Andy Butland e9819d6e57 Merge branch 'main' into v18/dev 2026-01-09 12:23:44 +01:00
Laura Neto 480a208655 Merge branch 'main' into v18/dev
# Conflicts:
#	src/Umbraco.Web.UI.Client/package-lock.json
#	src/Umbraco.Web.UI.Client/package.json
#	version.json
2026-01-06 14:52:30 +01:00
Laura Neto 179f2e709d Merge branch 'main' into v18/dev 2025-12-16 15:44:08 +01:00
Laura Neto 90c09b1bf1 Merge branch 'main' into v18/dev
# Conflicts:
#	tests/Umbraco.Tests.Integration/CompatibilitySuppressions.xml
2025-12-11 10:17:18 +01:00
Laura Neto e7719c2458 Cleanup compatibility suppressions 2025-12-04 11:15:19 +01:00
Laura Neto fbce5882c6 Set up new v18 branch 2025-12-04 11:03:26 +01:00
3437 changed files with 209562 additions and 84473 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
+30 -6
View File
@@ -1,28 +1,52 @@
name: Claude PR Review
on:
pull_request_target:
types: [opened, ready_for_review]
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
issues: write
id-token: write
actions: read
jobs:
review:
if: github.event.pull_request.draft == false
# 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@v4
- uses: actions/checkout@v6
with:
fetch-depth: 1
- uses: anthropics/claude-code-action@v1
with:
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY_03 }}
base_branch: "main"
# 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: |
+1 -1
View File
@@ -25,7 +25,7 @@ jobs:
(github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude')))
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
with:
fetch-depth: 1
+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:
+7 -3
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,14 +72,16 @@ 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
/src/Umbraco.Web.UI/wwwroot/[Mm]edia/
/src/Umbraco.Web.UI/App_Code/
/src/Umbraco.Web.UI/App_Plugins/
/src/Umbraco.Web.UI/[Uu]mbraco/[Dd]ata/
/src/Umbraco.Web.UI/[Uu]mbraco/[Dd]ata/*
!/src/Umbraco.Web.UI/[Uu]mbraco/[Dd]ata/Umbraco.Sample.sqlite.db
/src/Umbraco.Web.UI/[Uu]mbraco/[Ll]ogs/
/src/Umbraco.Web.UI/[Uu]mbraco/[Mm]odels/
/src/Umbraco.Web.UI/Views/
+49 -6
View File
@@ -46,7 +46,8 @@ Enterprise-grade CMS built on .NET 10.0. This repository contains 21 production
- **ASP.NET Core** - Web framework
- **Entity Framework Core** - Modern ORM
- **OpenIddict** - OAuth 2.0/OpenID Connect authentication
- **Swashbuckle** - OpenAPI/Swagger documentation
- **Microsoft.AspNetCore.OpenApi** - OpenAPI document generation
- **Swashbuckle.AspNetCore.SwaggerUI** - Swagger UI for API documentation
- **Lucene.NET** - Full-text search via Examine
- **ImageSharp** - Image processing
@@ -227,9 +228,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
@@ -364,16 +367,27 @@ public interface IMyService
### Centralized Package Management
**All NuGet package versions** are centralized in `Directory.Packages.props`. Individual projects do NOT specify versions.
**NuGet package versions** are centralized in `Directory.Packages.props`. There are two `Directory.Packages.props` files in the source tree, with multi-level merging enabled so the test file inherits from the root:
| File | Scope |
|------|-------|
| `Directory.Packages.props` (root) | Production source code packages — referenced by all `src/**` projects |
| `tests/Directory.Packages.props` | Test-only packages (NUnit, Moq, Bogus, BenchmarkDotNet, etc.) — adds entries on top of the inherited root file |
When updating dependencies, decide which file the package belongs in:
- A package used only by test projects → `tests/Directory.Packages.props`
- A package used by any production project (or by both production and tests) → root `Directory.Packages.props`
```xml
<!-- Individual projects reference WITHOUT version -->
<PackageReference Include="Swashbuckle.AspNetCore" />
<PackageReference Include="Microsoft.AspNetCore.OpenApi" />
<!-- Versions defined in Directory.Packages.props -->
<PackageVersion Include="Swashbuckle.AspNetCore" Version="6.5.0" />
<PackageVersion Include="Microsoft.AspNetCore.OpenApi" Version="10.0.0" />
```
**Opt-out**: `src/Umbraco.Web.UI/Umbraco.Web.UI.csproj` sets `<ManagePackageVersionsCentrally>false</ManagePackageVersionsCentrally>` and specifies versions inline (for `Microsoft.EntityFrameworkCore.Design`, `Microsoft.Build.Tasks.Core`, `Microsoft.ICU.ICU4C.Runtime`, etc.). Update those versions directly in that csproj when bumping. Two further `Directory.Packages.props` files exist under `templates/` for the project/extension templates and have their own version sets — keep `Microsoft.AspNetCore.OpenApi` aligned between the root file and `templates/UmbracoExtension/`.
### Build Configuration
- `Directory.Build.props` - Shared properties (target framework, company, copyright)
@@ -417,7 +431,8 @@ All APIs use **OpenIddict** (OAuth 2.0/OpenID Connect):
APIs use `Asp.Versioning.Mvc`:
- Management API: `/umbraco/management/api/v{version}/*`
- Delivery API: `/umbraco/delivery/api/v{version}/*`
- OpenAPI/Swagger docs per version
- OpenAPI docs: `/umbraco/openapi/management.json`, `/umbraco/openapi/delivery.json`
- Swagger UI: `/umbraco/openapi/`
### Updating `OpenApi.json` (Management API)
@@ -503,6 +518,32 @@ Labels are only added, never removed. Claude applies only labels it is confident
---
## 8. Code Comment Policy
**Default to no comment.** Applies to all code in this repository — C#, TypeScript, Razor, build scripts. Well-named identifiers and small functions are the primary form of self-documentation; comments are a fallback for the rare cases where the code itself cannot carry the meaning.
### When NOT to comment
- **Don't restate what the code does.** A line calling `resetState()` does not need `// Reset state`. A method named `validateInput` does not need `// Validate input`.
- **Don't narrate a sequence of calls.** If three lines run in order, the order is in the code — don't paraphrase it above.
- **Don't reference the current task, fix, callers, or PR.** No `// Fix for X`, `// Used by Y`, `// Added for the Z flow`, `// See PR #1234`. That belongs in commit messages and PR descriptions; in source it rots as the codebase evolves.
### When a comment IS justified
Write a comment only when **removing it would leave a future reader confused**. Concretely:
- **A non-obvious WHY.** A hidden constraint, business rule, or ordering requirement that is not visible from the code.
- **A workaround for a specific bug or platform quirk.** Link the issue (`(#21996)`, `https://...`) so the comment can be deleted once the upstream fix lands.
- **A subtle invariant** that the type system or method names do not enforce.
- **An edge case the code intentionally handles** that would surprise a reader (e.g. "must run before X because Y").
- **API documentation** — XML doc comments on C# members, JSDoc on exported TypeScript symbols. Required for the public contract; still keep them concise.
### TODOs
Allowed, but cheap to write and cheaper to leave behind. Keep them short and trackable: `// TODO (V19): remove once obsolete overload is gone` or `// TODO: pagination [NL]`. A TODO should have an author or a version trigger.
---
## Quick Reference
### Essential Commands
@@ -561,6 +602,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/
+2 -2
View File
@@ -40,8 +40,8 @@
<!-- Package Validation -->
<PropertyGroup>
<GenerateCompatibilitySuppressionFile>false</GenerateCompatibilitySuppressionFile>
<EnablePackageValidation>true</EnablePackageValidation>
<PackageValidationBaselineVersion>17.0.0</PackageValidationBaselineVersion>
<EnablePackageValidation>false</EnablePackageValidation> <!-- TODO (V18): Set to true once this version is released. -->
<PackageValidationBaselineVersion>18.0.0</PackageValidationBaselineVersion>
<EnableStrictModeForCompatibleFrameworksInPackage>true</EnableStrictModeForCompatibleFrameworksInPackage>
<EnableStrictModeForCompatibleTfms>true</EnableStrictModeForCompatibleTfms>
</PropertyGroup>
+42 -36
View File
@@ -8,50 +8,54 @@
<ItemGroup>
<GlobalPackageReference Include="Nerdbank.GitVersioning" Version="3.9.50" />
<GlobalPackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.556" />
<GlobalPackageReference Include="Umbraco.Code" Version="2.4.0" />
<!-- TODO (V18): Bump Umbraco.Code to 3.0.0 stable before release of 18.0.0 -->
<GlobalPackageReference Include="Umbraco.Code" Version="3.0.0-beta" />
<GlobalPackageReference Include="Umbraco.GitVersioning.Extensions" Version="0.2.0" />
</ItemGroup>
<!-- Microsoft packages -->
<ItemGroup>
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation" Version="10.0.4" />
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="4.14.0" />
<PackageVersion Include="Microsoft.Data.Sqlite" Version="10.0.4" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="10.0.4" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.SqlServer" Version="10.0.4" />
<PackageVersion Include="Microsoft.Extensions.Caching.Abstractions" Version="10.0.4" />
<PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="10.0.4" />
<PackageVersion Include="Microsoft.Extensions.Configuration.Abstractions" Version="10.0.4" />
<PackageVersion Include="Microsoft.Extensions.Configuration.Json" Version="10.0.4" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="10.0.4" />
<PackageVersion Include="Microsoft.Extensions.FileProviders.Embedded" Version="10.0.4" />
<PackageVersion Include="Microsoft.Extensions.FileProviders.Physical" Version="10.0.4" />
<PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="10.0.4" />
<PackageVersion Include="Microsoft.Extensions.Http" Version="10.0.4" />
<PackageVersion Include="Microsoft.Extensions.Identity.Core" Version="10.0.4" />
<PackageVersion Include="Microsoft.Extensions.Identity.Stores" Version="10.0.4" />
<PackageVersion Include="Microsoft.Extensions.Logging" Version="10.0.4" />
<PackageVersion Include="Microsoft.Extensions.Options" Version="10.0.4" />
<PackageVersion Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="10.0.4" />
<PackageVersion Include="Microsoft.Extensions.Options.DataAnnotations" Version="10.0.4" />
<PackageVersion Include="Microsoft.Extensions.Caching.Hybrid" Version="10.4.0" />
<PackageVersion Include="System.Linq.Async" Version="7.0.0" />
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation" Version="10.0.7" />
<!-- When updating this version, also update templates/UmbracoExtension/Umbraco.Extension.csproj -->
<PackageVersion Include="Microsoft.AspNetCore.OpenApi" Version="10.0.7" />
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="5.3.0" />
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="5.3.0" />
<PackageVersion Include="Microsoft.Data.Sqlite" Version="10.0.7" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="10.0.7" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.SqlServer" Version="10.0.7" />
<PackageVersion Include="Microsoft.Extensions.Caching.Abstractions" Version="10.0.7" />
<PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="10.0.7" />
<PackageVersion Include="Microsoft.Extensions.Configuration.Abstractions" Version="10.0.7" />
<PackageVersion Include="Microsoft.Extensions.Configuration.Json" Version="10.0.7" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="10.0.7" />
<PackageVersion Include="Microsoft.Extensions.FileProviders.Embedded" Version="10.0.7" />
<PackageVersion Include="Microsoft.Extensions.FileProviders.Physical" Version="10.0.7" />
<PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="10.0.7" />
<PackageVersion Include="Microsoft.Extensions.Http" Version="10.0.7" />
<PackageVersion Include="Microsoft.Extensions.Identity.Core" Version="10.0.7" />
<PackageVersion Include="Microsoft.Extensions.Identity.Stores" Version="10.0.7" />
<PackageVersion Include="Microsoft.Extensions.Logging" Version="10.0.7" />
<PackageVersion Include="Microsoft.Extensions.Options" Version="10.0.7" />
<PackageVersion Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="10.0.7" />
<PackageVersion Include="Microsoft.Extensions.Options.DataAnnotations" Version="10.0.7" />
<PackageVersion Include="Microsoft.Extensions.Caching.Hybrid" Version="10.5.0" />
<PackageVersion Include="System.Linq.Async" Version="7.0.1" />
</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>
<PackageVersion Include="Asp.Versioning.Mvc" Version="8.1.1" />
<PackageVersion Include="Asp.Versioning.Mvc.ApiExplorer" Version="8.1.1" />
<PackageVersion Include="Asp.Versioning.Mvc" Version="10.0.0" />
<PackageVersion Include="Asp.Versioning.Mvc.ApiExplorer" Version="10.0.0" />
<PackageVersion Include="Dazinator.Extensions.FileProviders" Version="2.0.0" />
<PackageVersion Include="Examine" Version="3.7.1" />
<PackageVersion Include="Examine.Core" Version="3.7.1" />
<PackageVersion Include="HtmlAgilityPack" Version="1.12.4" />
<PackageVersion Include="JsonPatch.Net" Version="3.3.0" />
<PackageVersion Include="K4os.Compression.LZ4" Version="1.3.8" />
<PackageVersion Include="MailKit" Version="4.15.1" />
<PackageVersion Include="Markdig" Version="0.45.0" />
<PackageVersion Include="MailKit" Version="4.16.0" />
<PackageVersion Include="Markdig" Version="1.1.3" />
<PackageVersion Include="Markdown" Version="2.2.1" />
<PackageVersion Include="MessagePack" Version="3.1.4" />
<PackageVersion Include="MiniProfiler.AspNetCore.Mvc" Version="4.5.4" />
@@ -59,25 +63,24 @@
<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.5.0" />
<PackageVersion Include="OpenIddict.AspNetCore" Version="7.5.0" />
<PackageVersion Include="OpenIddict.EntityFrameworkCore" Version="7.5.0" />
<PackageVersion Include="Serilog" Version="4.3.1" />
<PackageVersion Include="Serilog.AspNetCore" Version="9.0.0" />
<PackageVersion Include="Serilog.AspNetCore" Version="10.0.0" />
<PackageVersion Include="Serilog.Enrichers.Process" Version="3.0.0" />
<PackageVersion Include="Serilog.Enrichers.Thread" Version="4.0.0" />
<PackageVersion Include="Serilog.Expressions" Version="5.0.0" />
<PackageVersion Include="Serilog.Extensions.Hosting" Version="9.0.0" />
<PackageVersion Include="Serilog.Extensions.Hosting" Version="10.0.0" />
<PackageVersion Include="Serilog.Formatting.Compact" Version="3.0.0" />
<PackageVersion Include="Serilog.Formatting.Compact.Reader" Version="4.0.0" />
<PackageVersion Include="Serilog.Settings.Configuration" Version="9.0.0" />
<PackageVersion Include="Serilog.Settings.Configuration" Version="10.0.0" />
<PackageVersion Include="Serilog.Sinks.Async" Version="2.1.0" />
<PackageVersion Include="Serilog.Sinks.File" Version="7.0.0" />
<PackageVersion Include="Serilog.Sinks.Map" Version="2.0.0" />
<PackageVersion Include="SixLabors.ImageSharp" Version="3.1.12" />
<PackageVersion Include="SixLabors.ImageSharp.Web" Version="3.2.0" />
<!-- When updating this version, also update templates/UmbracoExtension/Umbraco.Extension.csproj -->
<PackageVersion Include="Swashbuckle.AspNetCore" Version="10.1.4" />
<PackageVersion Include="Swashbuckle.AspNetCore.SwaggerUI" Version="10.1.7" />
</ItemGroup>
<!-- Transitive pinned versions (only required because our direct dependencies have vulnerable versions of transitive dependencies) -->
<ItemGroup>
@@ -88,5 +91,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.7" />
</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
+21 -15
View File
@@ -4,11 +4,11 @@ pr: none
trigger: none
schedules:
- cron: '0 3 * * *'
displayName: Daily 3AM build (main)
- cron: '0 6 * * *'
displayName: Daily 6AM build (v18/dev)
branches:
include:
- main
- v18/dev
parameters:
- name: skipIntegrationTests
@@ -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:
@@ -199,31 +199,37 @@ stages:
SA_PASSWORD: UmbracoAcceptance123!
strategy:
matrix:
# We split the tests into 4 parts for each OS to reduce the time it takes to run them on the pipeline
WindowsPart1Of4:
# Windows is split into 5 parts (ManagementApi split in two to avoid memory pressure on LocalDb); Linux into 4.
WindowsPart1Of5:
vmImage: "windows-latest"
Tests__Database__DatabaseType: LocalDb
Tests__Database__SQLServerMasterConnectionString: N/A
# Filter tests that are part of the Umbraco.Infrastructure namespace but not part of the Umbraco.Infrastructure.Service namespace
testFilter: "(FullyQualifiedName~Umbraco.Infrastructure) & (FullyQualifiedName!~Umbraco.Infrastructure.Service)"
WindowsPart2Of4:
WindowsPart2Of5:
vmImage: "windows-latest"
Tests__Database__DatabaseType: LocalDb
Tests__Database__SQLServerMasterConnectionString: N/A
# Filter tests that are part of the Umbraco.Infrastructure.Service namespace
testFilter: "(FullyQualifiedName~Umbraco.Infrastructure.Service)"
WindowsPart3Of4:
WindowsPart3Of5:
vmImage: "windows-latest"
Tests__Database__DatabaseType: LocalDb
Tests__Database__SQLServerMasterConnectionString: N/A
# Filter tests that are not part of the Umbraco.Infrastructure and ManagementApi namespace.
testFilter: "(FullyQualifiedName!~Umbraco.Infrastructure) & (FullyQualifiedName!~ManagementApi)"
WindowsPart4Of4:
WindowsPart4Of5:
vmImage: "windows-latest"
Tests__Database__DatabaseType: LocalDb
Tests__Database__SQLServerMasterConnectionString: N/A
# Filter tests that are part of the ManagementApi namespace.
testFilter: "(FullyQualifiedName~ManagementApi)"
# ManagementApi, heavier sub-namespaces. Trailing dots prevent "User." from matching "UserGroup." etc.
testFilter: "FullyQualifiedName~ManagementApi & (FullyQualifiedName~ManagementApi.Element. | FullyQualifiedName~ManagementApi.User. | FullyQualifiedName~ManagementApi.Document. | FullyQualifiedName~ManagementApi.DataType. | FullyQualifiedName~ManagementApi.DocumentType. | FullyQualifiedName~ManagementApi.MediaType. | FullyQualifiedName~ManagementApi.Template.)"
WindowsPart5Of5:
vmImage: "windows-latest"
Tests__Database__DatabaseType: LocalDb
Tests__Database__SQLServerMasterConnectionString: N/A
# ManagementApi, remainder (complement of Part4). vstest filters do not support group
testFilter: "FullyQualifiedName~ManagementApi & FullyQualifiedName!~ManagementApi.Element. & FullyQualifiedName!~ManagementApi.User. & FullyQualifiedName!~ManagementApi.Document. & FullyQualifiedName!~ManagementApi.DataType. & FullyQualifiedName!~ManagementApi.DocumentType. & FullyQualifiedName!~ManagementApi.MediaType. & FullyQualifiedName!~ManagementApi.Template."
LinuxPart1Of4:
vmImage: "ubuntu-latest"
Tests__Database__DatabaseType: SqlServer
@@ -319,8 +325,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 +506,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
+42 -70
View File
@@ -13,7 +13,8 @@ Shared infrastructure for Umbraco CMS REST APIs (Management and Delivery).
### Key Technologies
- **ASP.NET Core** - Web framework
- **Swashbuckle** - OpenAPI/Swagger documentation generation
- **Microsoft.AspNetCore.OpenApi** - OpenAPI document generation
- **Swashbuckle.AspNetCore.SwaggerUI** - Swagger UI for browsing API documentation
- **OpenIddict** - OAuth 2.0/OpenID Connect authentication
- **Asp.Versioning** - API versioning
- **System.Text.Json** - Polymorphic JSON serialization
@@ -27,14 +28,18 @@ Shared infrastructure for Umbraco CMS REST APIs (Management and Delivery).
```
Umbraco.Cms.Api.Common/
├── OpenApi/ # Schema/Operation ID handlers for Swagger
│ ├── SchemaIdHandler.cs # Generates schema IDs (e.g., "PagedUserModel")
│ ├── OperationIdHandler.cs # Generates operation IDs
── SubTypesHandler.cs # Polymorphism support
├── OpenApi/ # OpenAPI transformers and schema generators
│ ├── UmbracoSchemaIdGenerator.cs # Generates schema IDs (e.g., "PagedUserModel")
│ ├── UmbracoOperationIdTransformer.cs # Generates operation IDs
── SortTagsAndPathsTransformer.cs # Sorts OpenAPI tags and paths
│ ├── TagActionsByGroupNameTransformer.cs # Tags operations by controller group
│ ├── FixFileReturnTypesTransformer.cs # Fixes file return type schemas
│ ├── RequireNonNullablePropertiesSchemaTransformer.cs # Schema nullability
│ └── OpenApiRouteTemplatePipelineFilter.cs # Adds OpenAPI endpoints
├── Serialization/ # JSON type resolution
│ └── UmbracoJsonTypeInfoResolver.cs
├── Configuration/ # Options configuration
│ ├── ConfigureUmbracoSwaggerGenOptions.cs
│ ├── ConfigureUmbracoOpenApiOptionsBase.cs
│ └── ConfigureOpenIddict.cs
├── DependencyInjection/ # Service registration
│ ├── UmbracoBuilderApiExtensions.cs
@@ -47,9 +52,8 @@ Umbraco.Cms.Api.Common/
### Design Patterns
1. **Strategy Pattern** - `ISchemaIdHandler`, `IOperationIdHandler` (extensible via inheritance)
2. **Builder Pattern** - `ProblemDetailsBuilder` for fluent error responses
3. **Options Pattern** - All configuration via `IConfigureOptions<T>`
1. **Builder Pattern** - `ProblemDetailsBuilder` for fluent error responses
2. **Options Pattern** - All configuration via `IConfigureOptions<T>`
---
@@ -61,25 +65,12 @@ See "Quick Reference" section at bottom for common commands.
## 3. Key Patterns
### Virtual Handlers for Extensibility
### Schema ID Generation (OpenApi/UmbracoSchemaIdGenerator.cs)
Handlers are intentionally virtual to allow consuming APIs to override:
Static utility class that generates OpenAPI schema IDs following Umbraco's naming conventions:
```csharp
// NOTE: Left unsealed on purpose, so it is extendable.
public class SchemaIdHandler : ISchemaIdHandler
{
public virtual bool CanHandle(Type type) { }
public virtual string Handle(Type type) { }
}
```
**Why**: Management and Delivery APIs can customize schema/operation ID generation.
### Schema ID Sanitization (OpenApi/SchemaIdHandler.cs:24-29, 32)
```csharp
// Add "Model" suffix to avoid TypeScript name clashes (lines 24-29)
// Add "Model" suffix to avoid TypeScript name clashes
if (name.EndsWith("Model") == false)
{
// because some models names clash with common classes in TypeScript (i.e. Document),
@@ -87,10 +78,12 @@ if (name.EndsWith("Model") == false)
name = $"{name}Model";
}
// Remove invalid characters to prevent OpenAPI generation errors (line 32)
// Remove invalid characters to prevent OpenAPI generation errors
return Regex.Replace(name, @"[^\w]", string.Empty);
```
**Generic Type Handling**: `PagedViewModel<RelationItemViewModel>` becomes `PagedRelationItemModel`
### Polymorphic Deserialization (Serialization/UmbracoJsonTypeInfoResolver.cs:29-35)
```csharp
@@ -116,9 +109,12 @@ if (type.IsInterface is false)
dotnet test tests/Umbraco.Tests.Integration/
# Verify OpenAPI generation
# 1. Run Management API
# 2. Navigate to /umbraco/swagger/
# 1. Run the application: dotnet run --project src/Umbraco.Web.UI
# 2. Navigate to /umbraco/openapi/ for Swagger UI
# 3. Check schema IDs and operation IDs
# OpenAPI JSON documents available at:
# - /umbraco/openapi/management.json (Management API)
# - /umbraco/openapi/delivery.json (Delivery API)
```
**Focus areas when testing**:
@@ -207,49 +203,24 @@ catch (NotSupportedException exception)
**Issue**: Type names like `Document` clash with TypeScript built-ins.
**Solution**: Add "Model" suffix (OpenApi/SchemaIdHandler.cs:24-29)
**Solution**: `UmbracoSchemaIdGenerator` adds "Model" suffix to all schema names.
### Generic Type Handling
**Issue**: `PagedViewModel<T>` needs flattened schema name.
**Solution** (OpenApi/SchemaIdHandler.cs:41-50):
```csharp
private string HandleGenerics(string name, Type type)
{
if (!type.IsGenericType)
return name;
// use attribute custom name or append the generic type names
// turns "PagedViewModel<RelationItemViewModel>" into "PagedRelationItem"
return $"{name}{string.Join(string.Empty, type.GenericTypeArguments.Select(SanitizedTypeName))}";
}
```
**Solution**: `UmbracoSchemaIdGenerator.Generate()` flattens generic types:
- `PagedViewModel<RelationItemViewModel>` becomes `PagedRelationItemModel`
---
## 7. Extending This Library
### Adding a Custom OpenAPI Handler
### Adding Custom OpenAPI Transformers
1. **Implement interface**:
```csharp
public class MySchemaIdHandler : SchemaIdHandler
{
public override bool CanHandle(Type type)
=> type.Namespace?.StartsWith("MyProject") is true;
OpenAPI transformers are scoped per-document. To customize a document, implement `IOpenApiDocumentTransformer`, `IOpenApiOperationTransformer`, or `IOpenApiSchemaTransformer` and register with your OpenAPI options.
public override string Handle(Type type)
=> $"My{base.Handle(type)}";
}
```
2. **Register in consuming API**:
```csharp
builder.Services.AddSingleton<ISchemaIdHandler, MySchemaIdHandler>();
```
**Note**: Handlers registered later take precedence in the selector.
For schema ID generation, use the static `UmbracoSchemaIdGenerator.Generate(Type)` method.
### Customizing Problem Details
@@ -269,13 +240,9 @@ return BadRequest(problemDetails);
## 8. Project-Specific Notes
### Why Virtual Handlers?
### Per-Document Transformer Scoping
**Decision**: Make `SchemaIdHandler`, `OperationIdHandler`, etc. virtual.
**Why**: Management API and Delivery API have different schema ID requirements. Virtual methods allow override without rewriting the entire handler.
**Example**: Management API might prefix all schemas with "Management", Delivery API with "Delivery".
With Microsoft.AspNetCore.OpenApi, transformers are configured per OpenAPI document. This means custom transformers only apply to the documents they're registered with, not globally. Each API (Management, Delivery) configures its own transformers via `ConfigureUmbracoOpenApiOptionsBase` subclasses.
### Performance: Subtype Caching
@@ -304,9 +271,13 @@ return BadRequest(problemDetails);
- Version: See `Directory.Packages.props`
- Uses ASP.NET Core Data Protection for token encryption
**Swashbuckle**:
- OpenAPI 3.0 document generation
- Custom filters: `EnumSchemaFilter`, `MimeTypeDocumentFilter`, `RemoveSecuritySchemesDocumentFilter`
**Microsoft.AspNetCore.OpenApi**:
- OpenAPI 3.1.1 document generation
- Custom transformers: `SchemaIdTransformer`, `OperationIdTransformer`, `MimeTypeDocumentTransformer`, `ServerTransformer`
**Swashbuckle.AspNetCore.SwaggerUI**:
- Swagger UI for browsing and testing API endpoints
- Accessed at `/umbraco/openapi/`
**Asp.Versioning**:
- API versioning via `ApiVersion` attribute
@@ -318,7 +289,7 @@ return BadRequest(problemDetails);
### Usage Pattern
Consuming APIs call `builder.AddUmbracoApiOpenApiUI().AddUmbracoOpenIddict()`
Consuming APIs call `builder.AddUmbracoOpenApi().AddUmbracoOpenIddict()`
---
@@ -346,7 +317,8 @@ dotnet list src/Umbraco.Cms.Api.Common/Umbraco.Cms.Api.Common.csproj package --v
| Class | Purpose | File |
|-------|---------|------|
| `ProblemDetailsBuilder` | Build RFC 7807 error responses | Builders/ProblemDetailsBuilder.cs |
| `SchemaIdHandler` | Generate OpenAPI schema IDs | OpenApi/SchemaIdHandler.cs |
| `UmbracoSchemaIdGenerator` | Generate OpenAPI schema IDs | OpenApi/UmbracoSchemaIdGenerator.cs |
| `UmbracoOperationIdTransformer` | Generate operation IDs | OpenApi/UmbracoOperationIdTransformer.cs |
| `UmbracoJsonTypeInfoResolver` | Polymorphic JSON serialization | Serialization/UmbracoJsonTypeInfoResolver.cs |
| `UmbracoBuilderAuthExtensions` | Configure OpenIddict | DependencyInjection/UmbracoBuilderAuthExtensions.cs |
| `HideBackOfficeTokensHandler` | Secure cookie-based token storage | DependencyInjection/HideBackOfficeTokensHandler.cs |
@@ -0,0 +1,47 @@
using System.Reflection;
using Asp.Versioning;
using Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.AspNetCore.Mvc.ApiExplorer;
using Microsoft.AspNetCore.Mvc.Controllers;
using Umbraco.Cms.Api.Common.OpenApi;
namespace Umbraco.Cms.Api.Common.Configuration;
/// <summary>
/// Configures the OpenAPI options for the Default API.
/// </summary>
internal class ConfigureDefaultApiOptions : ConfigureUmbracoOpenApiOptionsBase
{
/// <inheritdoc />
protected override string ApiName => DefaultApiConfiguration.ApiName;
/// <inheritdoc />
protected override string ApiTitle => "Default API";
/// <inheritdoc />
protected override string ApiVersion => "Latest";
/// <inheritdoc />
protected override string ApiDescription => "All endpoints not defined under specific APIs";
/// <inheritdoc />
protected override bool ShouldInclude(ApiDescription apiDescription)
{
// Exclude controllers with ExcludeFromDefaultOpenApiDocumentAttribute
if (apiDescription.ActionDescriptor is ControllerActionDescriptor controllerActionDescriptor
&& controllerActionDescriptor.ControllerTypeInfo.GetCustomAttribute<ExcludeFromDefaultOpenApiDocumentAttribute>() is not null)
{
return false;
}
// Include if explicitly mapped to this document
if (base.ShouldInclude(apiDescription))
{
return true;
}
// Include endpoints not explicitly assigned to another document
ApiVersionMetadata apiVersionMetadata = apiDescription.ActionDescriptor.ApiVersionMetadata;
return string.IsNullOrEmpty(apiVersionMetadata.Name);
}
}
@@ -0,0 +1,126 @@
using System.Text.Json.Serialization.Metadata;
using Asp.Versioning;
using Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.AspNetCore.Mvc.ApiExplorer;
using Microsoft.AspNetCore.Mvc.Controllers;
using Microsoft.AspNetCore.OpenApi;
using Microsoft.Extensions.Options;
using Microsoft.OpenApi;
using Umbraco.Cms.Api.Common.OpenApi;
using Umbraco.Extensions;
namespace Umbraco.Cms.Api.Common.Configuration;
/// <summary>
/// Base class for configuring OpenAPI options for Umbraco APIs.
/// </summary>
internal abstract class ConfigureUmbracoOpenApiOptionsBase : IConfigureNamedOptions<OpenApiOptions>
{
/// <summary>
/// Gets the name/identifier of the API to configure.
/// </summary>
protected abstract string ApiName { get; }
/// <summary>
/// Gets the name/identifier of the API to configure.
/// </summary>
protected abstract string ApiTitle { get; }
/// <summary>
/// Gets the version of the API to configure.
/// </summary>
protected abstract string ApiVersion { get; }
/// <summary>
/// Gets the description of the API to configure.
/// </summary>
protected abstract string ApiDescription { get; }
/// <inheritdoc />
public void Configure(OpenApiOptions options) => Configure(Options.DefaultName, options);
/// <inheritdoc />
public void Configure(string? name, OpenApiOptions options)
{
if (name != ApiName)
{
return;
}
ConfigureOpenApi(options);
}
/// <summary>
/// Configure the OpenAPI options for the specified API.
/// </summary>
/// <param name="options">The <see cref="OpenApiOptions"/> instance to configure.</param>
protected virtual void ConfigureOpenApi(OpenApiOptions options)
{
options.AddDocumentTransformer((document, _, _) =>
{
document.Info = new OpenApiInfo
{
Title = ApiTitle,
Version = ApiVersion,
Description = ApiDescription,
};
document.Servers?.Clear();
return Task.CompletedTask;
});
options.ShouldInclude = ShouldInclude;
options.CreateSchemaReferenceId = CreateSchemaReferenceId;
options.AddOperationTransformer<UmbracoOperationIdTransformer>();
// Tag actions by group name and cleanup unused tags (caused by the tag changes)
options
.AddOperationTransformer<TagActionsByGroupNameTransformer>()
.AddDocumentTransformer<TagActionsByGroupNameTransformer>()
.AddDocumentTransformer<SortTagsAndPathsTransformer>();
}
/// <summary>
/// Creates a schema reference ID for the given JSON type info.
/// Returns null for types that should be inlined, the default schema ID for non-Umbraco types,
/// or a generated schema ID for Umbraco types.
/// </summary>
/// <param name="jsonTypeInfo">The JSON type info to create a schema reference ID for.</param>
/// <returns>The schema reference ID, or null if the type should be inlined.</returns>
internal static string? CreateSchemaReferenceId(JsonTypeInfo jsonTypeInfo)
{
// Ensure that only types that would normally be included in the schema generation are given a schema reference ID.
// Otherwise, we should return null to inline them.
var defaultSchemaReferenceId = OpenApiOptions.CreateDefaultSchemaReferenceId(jsonTypeInfo);
if (defaultSchemaReferenceId is null)
{
return null;
}
Type targetType = Nullable.GetUnderlyingType(jsonTypeInfo.Type) ?? jsonTypeInfo.Type;
if (targetType.Namespace?.StartsWith("Umbraco.Cms") is not true)
{
return defaultSchemaReferenceId;
}
return UmbracoSchemaIdGenerator.Generate(targetType);
}
/// <summary>
/// Determines whether the specified API description should be included in this OpenAPI document.
/// </summary>
/// <param name="apiDescription">The API description to evaluate.</param>
/// <returns><c>true</c> if the endpoint should be included; otherwise, <c>false</c>.</returns>
protected virtual bool ShouldInclude(ApiDescription apiDescription)
{
if (apiDescription.ActionDescriptor is ControllerActionDescriptor controllerActionDescriptor
&& controllerActionDescriptor.HasMapToApiAttribute(ApiName))
{
return true;
}
ApiVersionMetadata apiVersionMetadata = apiDescription.ActionDescriptor.ApiVersionMetadata;
return apiVersionMetadata.Name == ApiName;
}
}
@@ -1,94 +0,0 @@
using Microsoft.AspNetCore.Mvc.ApiExplorer;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using Microsoft.OpenApi;
using Swashbuckle.AspNetCore.SwaggerGen;
using Umbraco.Cms.Api.Common.OpenApi;
using Umbraco.Cms.Core.DependencyInjection;
namespace Umbraco.Cms.Api.Common.Configuration;
/// <summary>
/// Configures Swagger/OpenAPI generation options for Umbraco APIs.
/// </summary>
public class ConfigureUmbracoSwaggerGenOptions : IConfigureOptions<SwaggerGenOptions>
{
private readonly IOperationIdSelector _operationIdSelector;
private readonly ISchemaIdSelector _schemaIdSelector;
private readonly ISubTypesSelector _subTypesSelector;
private readonly IDocumentInclusionSelector _documentInclusionSelector;
/// <summary>
/// Initializes a new instance of the <see cref="ConfigureUmbracoSwaggerGenOptions"/> class.
/// </summary>
/// <param name="operationIdSelector">The operation ID selector.</param>
/// <param name="schemaIdSelector">The schema ID selector.</param>
/// <param name="subTypesSelector">The sub-types selector for polymorphism support.</param>
/// <param name="documentInclusionSelector">The document inclusion selector.</param>
public ConfigureUmbracoSwaggerGenOptions(
IOperationIdSelector operationIdSelector,
ISchemaIdSelector schemaIdSelector,
ISubTypesSelector subTypesSelector,
IDocumentInclusionSelector documentInclusionSelector)
{
_operationIdSelector = operationIdSelector;
_schemaIdSelector = schemaIdSelector;
_subTypesSelector = subTypesSelector;
_documentInclusionSelector = documentInclusionSelector;
}
/// <summary>
/// Initializes a new instance of the <see cref="ConfigureUmbracoSwaggerGenOptions"/> class.
/// </summary>
/// <param name="operationIdSelector">The operation ID selector.</param>
/// <param name="schemaIdSelector">The schema ID selector.</param>
/// <param name="subTypesSelector">The sub-types selector for polymorphism support.</param>
[Obsolete("Please use the constructor with all parameters. Scheduled for removal in Umbraco 19.")]
public ConfigureUmbracoSwaggerGenOptions(
IOperationIdSelector operationIdSelector,
ISchemaIdSelector schemaIdSelector,
ISubTypesSelector subTypesSelector)
: this(
operationIdSelector,
schemaIdSelector,
subTypesSelector,
StaticServiceProvider.Instance.GetRequiredService<IDocumentInclusionSelector>())
{
}
/// <inheritdoc/>
public void Configure(SwaggerGenOptions swaggerGenOptions)
{
swaggerGenOptions.SwaggerDoc(
DefaultApiConfiguration.ApiName,
new OpenApiInfo
{
Title = "Default API",
Version = "Latest",
Description = "All endpoints not defined under specific APIs",
});
swaggerGenOptions.CustomOperationIds(description => _operationIdSelector.OperationId(description));
swaggerGenOptions.DocInclusionPredicate(_documentInclusionSelector.Include);
swaggerGenOptions.TagActionsBy(api =>
api.GroupName is null
? []
: new[] { api.GroupName });
swaggerGenOptions.OrderActionsBy(ActionOrderBy);
swaggerGenOptions.SchemaFilter<EnumSchemaFilter>();
swaggerGenOptions.CustomSchemaIds(_schemaIdSelector.SchemaId);
swaggerGenOptions.SelectSubTypesUsing(_subTypesSelector.SubTypes);
swaggerGenOptions.SupportNonNullableReferenceTypes();
}
/// <summary>
/// Generates a sort key for API actions.
/// </summary>
/// <param name="apiDesc">The API description.</param>
/// <returns>A string used to sort API operations in the documentation.</returns>
/// <remarks>
/// See https://github.com/domaindrivendev/Swashbuckle.AspNetCore#change-operation-sort-order-eg-for-ui-sorting.
/// </remarks>
private static string ActionOrderBy(ApiDescription apiDesc)
=> $"{apiDesc.GroupName}_{apiDesc.ActionDescriptor.AttributeRouteInfo?.Template ?? apiDesc.ActionDescriptor.RouteValues["controller"]}_{(apiDesc.ActionDescriptor.RouteValues.TryGetValue("action", out var action) ? action : null)}_{apiDesc.HttpMethod}";
}
@@ -0,0 +1,54 @@
using Microsoft.AspNetCore.Http.Json;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
namespace Umbraco.Cms.Api.Common.DependencyInjection;
/// <summary>
/// Extension methods for replacing the internal Microsoft.AspNetCore.OpenApi schema service registration.
/// </summary>
internal static class OpenApiSchemaServiceExtensions
{
/// <summary>
/// The full name of the internal Microsoft type whose registration is replaced.
/// Used for a stringly-typed <see cref="ServiceDescriptor"/> lookup because the type is not publicly accessible.
/// </summary>
internal const string OpenApiSchemaServiceFullName = "Microsoft.AspNetCore.OpenApi.OpenApiSchemaService";
/// <summary>
/// Replaces the internal Microsoft <c>OpenApiSchemaService</c> registration for the specified document so that schema
/// generation uses the named <see cref="JsonOptions"/> rather than the default HTTP JSON options.
/// </summary>
/// <param name="services">The service collection.</param>
/// <param name="documentName">The OpenAPI document key (matches the keyed singleton registered by <c>AddOpenApi(documentName)</c>).</param>
/// <param name="jsonOptionsName">The named <see cref="JsonOptions"/> to use during schema generation for this document.</param>
/// <returns>The same <see cref="IServiceCollection"/> for chaining.</returns>
/// <remarks>
/// Workaround for <see href="https://github.com/dotnet/aspnetcore/issues/66340">dotnet/aspnetcore#66340</see>.
/// </remarks>
public static IServiceCollection ReplaceOpenApiSchemaService(
this IServiceCollection services,
string documentName,
string jsonOptionsName)
{
ServiceDescriptor descriptor = services.FirstOrDefault(sd =>
sd.ServiceType.FullName == OpenApiSchemaServiceFullName
&& Equals(sd.ServiceKey, documentName))
?? throw new InvalidOperationException(
$"Could not find a registration for {OpenApiSchemaServiceFullName} keyed with '{documentName}'. "
+ $"Ensure AddOpenApi(\"{documentName}\") has been called before {nameof(ReplaceOpenApiSchemaService)}, "
+ "or check whether the internal Microsoft.AspNetCore.OpenApi registration shape has changed.");
services.Remove(descriptor);
services.AddKeyedSingleton(
descriptor.ServiceType,
documentName,
(sp, key) => ActivatorUtilities.CreateInstance(
sp,
descriptor.ServiceType,
key,
Options.Create(sp.GetRequiredService<IOptionsMonitor<JsonOptions>>().Get(jsonOptionsName))));
return services;
}
}
@@ -0,0 +1,37 @@
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using Swashbuckle.AspNetCore.SwaggerUI;
using Umbraco.Cms.Api.Common.OpenApi;
using Umbraco.Extensions;
namespace Umbraco.Cms.Api.Common.DependencyInjection;
/// <summary>
/// Extension methods for <see cref="IServiceCollection"/> to configure OpenAPI services.
/// </summary>
public static class OpenApiServiceCollectionExtensions
{
/// <summary>
/// Adds an OpenAPI document to the OpenAPI UI document selector dropdown.
/// </summary>
/// <param name="services">The <see cref="IServiceCollection"/> instance.</param>
/// <param name="documentName">The name/identifier of the OpenAPI document.</param>
/// <param name="documentTitle">The title to display in the UI dropdown. Defaults to <paramref name="documentName"/> if not specified.</param>
/// <returns>The <see cref="IServiceCollection"/> instance.</returns>
public static IServiceCollection AddOpenApiDocumentToUi(
this IServiceCollection services,
string documentName,
string? documentTitle = null)
{
services.AddOptions<SwaggerUIOptions>()
.Configure<IOptions<UmbracoOpenApiOptions>>((swaggerUiOptions, openApiOptions) =>
{
var openApiRoute = openApiOptions.Value.RouteTemplate.Replace("{documentName}", documentName).EnsureStartsWith("/");
swaggerUiOptions.SwaggerEndpoint(openApiRoute, documentTitle ?? documentName);
swaggerUiOptions.ConfigObject.Urls = swaggerUiOptions.ConfigObject.Urls.OrderBy(x => x.Name);
});
return services;
}
}
@@ -1,9 +1,14 @@
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Umbraco.Cms.Api.Common.Configuration;
using Umbraco.Cms.Api.Common.OpenApi;
using Umbraco.Cms.Api.Common.Serialization;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Core.Hosting;
using Umbraco.Cms.Web.Common.ApplicationBuilder;
using IHostingEnvironment = Umbraco.Cms.Core.Hosting.IHostingEnvironment;
namespace Umbraco.Cms.Api.Common.DependencyInjection;
@@ -16,26 +21,51 @@ public static class UmbracoBuilderApiExtensions
/// Adds Umbraco API OpenAPI/Swagger UI services to the builder.
/// </summary>
/// <param name="builder">The Umbraco builder.</param>
/// <returns>The Umbraco builder for method chaining.</returns>
public static IUmbracoBuilder AddUmbracoApiOpenApiUI(this IUmbracoBuilder builder)
internal static void AddUmbracoOpenApi(this IUmbracoBuilder builder)
{
if (builder.Services.Any(x => !x.IsKeyedService && x.ImplementationType == typeof(OperationIdSelector)))
if (builder.Services.Any(x => !x.IsKeyedService && x.ImplementationType == typeof(UmbracoJsonTypeInfoResolver)))
{
return builder;
return;
}
builder.Services.AddSwaggerGen();
builder.Services.ConfigureOptions<ConfigureUmbracoSwaggerGenOptions>();
builder.Services.AddOptions<UmbracoOpenApiOptions>()
.Configure<IHostingEnvironment, IWebHostEnvironment>((options, hostingEnv, webHostEnv) =>
{
options.Enabled = webHostEnv.IsProduction() is false;
var backOfficePath = hostingEnv.GetBackOfficePath().TrimStart(Constants.CharArrays.ForwardSlash);
options.RouteTemplate = $"{backOfficePath}/openapi/{{documentName}}.json";
options.UiRoutePrefix = $"{backOfficePath}/openapi";
});
builder.AddUmbracoOpenApiDocument<ConfigureDefaultApiOptions>(DefaultApiConfiguration.ApiName, "Default API");
builder.Services.AddSingleton<IUmbracoJsonTypeInfoResolver, UmbracoJsonTypeInfoResolver>();
builder.Services.AddSingleton<IOperationIdSelector, OperationIdSelector>();
builder.Services.AddSingleton<IOperationIdHandler, OperationIdHandler>();
builder.Services.AddSingleton<ISchemaIdSelector, SchemaIdSelector>();
builder.Services.AddSingleton<ISchemaIdHandler, SchemaIdHandler>();
builder.Services.AddSingleton<ISubTypesSelector, SubTypesSelector>();
builder.Services.AddSingleton<ISubTypesHandler, SubTypesHandler>();
builder.Services.AddSingleton<IDocumentInclusionSelector, DocumentInclusionSelector>();
builder.Services.Configure<UmbracoPipelineOptions>(options => options.AddFilter(new SwaggerRouteTemplatePipelineFilter("UmbracoApiCommon")));
builder.Services.Configure<UmbracoPipelineOptions>(options => options.AddFilter(new OpenApiRouteTemplatePipelineFilter("UmbracoApiCommon")));
}
return builder;
/// <summary>
/// Adds and configures an Umbraco OpenAPI document with shared transformers.
/// </summary>
/// <param name="builder">The Umbraco builder.</param>
/// <param name="apiName">The name/identifier of the API.</param>
/// <param name="apiTitle">The title of the API.</param>
/// <param name="jsonOptionsName">
/// Optional named <c>JsonOptions</c> to use for schema generation instead of the default HTTP JSON options.
/// When specified, replaces the internal <c>OpenApiSchemaService</c> registration for this document.
/// </param>
/// <typeparam name="TConfigureOptions">The type used to configure the OpenAPI options.</typeparam>
internal static void AddUmbracoOpenApiDocument<TConfigureOptions>(
this IUmbracoBuilder builder,
string apiName,
string apiTitle,
string? jsonOptionsName = null)
where TConfigureOptions : ConfigureUmbracoOpenApiOptionsBase
{
builder.Services.AddOpenApi(apiName);
builder.Services.ConfigureOptions<TConfigureOptions>();
builder.Services.AddOpenApiDocumentToUi(apiName, apiTitle);
if (jsonOptionsName is not null)
{
builder.Services.ReplaceOpenApiSchemaService(apiName, jsonOptionsName);
}
}
}
@@ -1,30 +0,0 @@
using Asp.Versioning;
using Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.AspNetCore.Mvc.ApiExplorer;
using Microsoft.AspNetCore.Mvc.Controllers;
using Umbraco.Cms.Api.Common.Configuration;
using Umbraco.Extensions;
namespace Umbraco.Cms.Api.Common.OpenApi;
/// <summary>
/// Determines whether an API description should be included in a specific documentation set based on the document name
/// and API metadata.
/// </summary>
public class DocumentInclusionSelector : IDocumentInclusionSelector
{
/// <inheritdoc/>
public bool Include(string documentName, ApiDescription apiDescription)
{
if (apiDescription.ActionDescriptor is ControllerActionDescriptor controllerActionDescriptor
&& controllerActionDescriptor.HasMapToApiAttribute(documentName))
{
return true;
}
ApiVersionMetadata apiVersionMetadata = apiDescription.ActionDescriptor.GetApiVersionMetadata();
return apiVersionMetadata.Name == documentName
|| (string.IsNullOrEmpty(apiVersionMetadata.Name) && documentName == DefaultApiConfiguration.ApiName);
}
}
@@ -1,35 +0,0 @@
using System.Reflection;
using System.Runtime.Serialization;
using System.Text.Json.Nodes;
using Microsoft.OpenApi;
using Swashbuckle.AspNetCore.SwaggerGen;
namespace Umbraco.Cms.Api.Common.OpenApi;
/// <summary>
/// A schema filter that converts enum schemas to string type with enum member names.
/// </summary>
/// <remarks>
/// This filter ensures enums are represented as strings in the OpenAPI schema,
/// using <see cref="EnumMemberAttribute"/> values when available.
/// </remarks>
public class EnumSchemaFilter : ISchemaFilter
{
/// <inheritdoc/>
public void Apply(IOpenApiSchema model, SchemaFilterContext context)
{
if (model is not OpenApiSchema schema || context.Type.IsEnum is false)
{
return;
}
schema.Type = JsonSchemaType.String;
schema.Format = null;
schema.Enum = new List<JsonNode>();
foreach (var name in Enum.GetNames(context.Type))
{
var actualName = context.Type.GetField(name)?.GetCustomAttribute<EnumMemberAttribute>()?.Value ?? name;
schema.Enum.Add(actualName);
}
}
}
@@ -0,0 +1,10 @@
namespace Umbraco.Cms.Api.Common.OpenApi;
/// <summary>
/// Excludes the controller from the default OpenAPI document.
/// Use this when you have a custom OpenAPI document for your API.
/// </summary>
[AttributeUsage(AttributeTargets.Class)]
public sealed class ExcludeFromDefaultOpenApiDocumentAttribute : Attribute
{
}
@@ -0,0 +1,47 @@
using System.IO.Pipelines;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.OpenApi;
using Microsoft.OpenApi;
namespace Umbraco.Cms.Api.Common.OpenApi;
/// <summary>
/// Transformer to fix file return types in OpenAPI schema.
/// </summary>
/// <remarks>Can be removed once https://github.com/dotnet/aspnetcore/pull/63504 and
/// https://github.com/dotnet/aspnetcore/pull/64562 are released.</remarks>
internal class FixFileReturnTypesTransformer : IOpenApiSchemaTransformer
{
private static readonly Type[] _binaryStringTypes =
[
typeof(IFormFile),
typeof(FileResult),
typeof(Stream),
typeof(PipeReader),
];
/// <inheritdoc />
public Task TransformAsync(
OpenApiSchema schema,
OpenApiSchemaTransformerContext context,
CancellationToken cancellationToken)
{
if (_binaryStringTypes.Any(possibleBaseType => possibleBaseType.IsAssignableFrom(context.JsonTypeInfo.Type)) is false)
{
return Task.CompletedTask;
}
// Clear all properties
schema.Properties?.Clear();
schema.Required?.Clear();
// Make it an inline schema
schema.Metadata?.Remove("x-schema-id");
// Set type to string with binary format
schema.Type = JsonSchemaType.String;
schema.Format = "binary";
return Task.CompletedTask;
}
}
@@ -1,19 +0,0 @@
using Microsoft.AspNetCore.Mvc.ApiExplorer;
namespace Umbraco.Cms.Api.Common.OpenApi;
/// <summary>
/// Defines a method that determines whether a given API description should be included in a specific documentation
/// document.
/// </summary>
public interface IDocumentInclusionSelector
{
/// <summary>
/// Determines whether the specified API description should be included in the generated documentation for the given
/// document name.
/// </summary>
/// <param name="documentName">The name of the documentation document being generated.</param>
/// <param name="apiDescription">The API description to evaluate for inclusion.</param>
/// <returns>true if the API description should be included in the documentation; otherwise, false.</returns>
bool Include(string documentName, ApiDescription apiDescription);
}
@@ -1,23 +0,0 @@
using Microsoft.AspNetCore.Mvc.ApiExplorer;
namespace Umbraco.Cms.Api.Common.OpenApi;
/// <summary>
/// Defines a handler for generating OpenAPI operation IDs.
/// </summary>
public interface IOperationIdHandler
{
/// <summary>
/// Determines whether this handler can generate an operation ID for the specified API description.
/// </summary>
/// <param name="apiDescription">The API description to check.</param>
/// <returns><c>true</c> if this handler can handle the API description; otherwise, <c>false</c>.</returns>
bool CanHandle(ApiDescription apiDescription);
/// <summary>
/// Generates an operation ID for the specified API description.
/// </summary>
/// <param name="apiDescription">The API description to generate an operation ID for.</param>
/// <returns>The generated operation ID.</returns>
string Handle(ApiDescription apiDescription);
}
@@ -1,17 +0,0 @@
using Asp.Versioning;
using Microsoft.AspNetCore.Mvc.ApiExplorer;
namespace Umbraco.Cms.Api.Common.OpenApi;
/// <summary>
/// Defines a selector for choosing operation IDs from registered handlers.
/// </summary>
public interface IOperationIdSelector
{
/// <summary>
/// Selects an operation ID for the specified API description.
/// </summary>
/// <param name="apiDescription">The API description to generate an operation ID for.</param>
/// <returns>The operation ID, or <c>null</c> if none could be determined.</returns>
string? OperationId(ApiDescription apiDescription);
}
@@ -1,21 +0,0 @@
namespace Umbraco.Cms.Api.Common.OpenApi;
/// <summary>
/// Defines a handler for generating OpenAPI schema IDs.
/// </summary>
public interface ISchemaIdHandler
{
/// <summary>
/// Determines whether this handler can generate a schema ID for the specified type.
/// </summary>
/// <param name="type">The type to check.</param>
/// <returns><c>true</c> if this handler can handle the type; otherwise, <c>false</c>.</returns>
bool CanHandle(Type type);
/// <summary>
/// Generates a schema ID for the specified type.
/// </summary>
/// <param name="type">The type to generate a schema ID for.</param>
/// <returns>The generated schema ID.</returns>
string Handle(Type type);
}
@@ -1,14 +0,0 @@
namespace Umbraco.Cms.Api.Common.OpenApi;
/// <summary>
/// Defines a selector for choosing schema IDs from registered handlers.
/// </summary>
public interface ISchemaIdSelector
{
/// <summary>
/// Selects a schema ID for the specified type.
/// </summary>
/// <param name="type">The type to generate a schema ID for.</param>
/// <returns>The schema ID.</returns>
string SchemaId(Type type);
}
@@ -1,22 +0,0 @@
namespace Umbraco.Cms.Api.Common.OpenApi;
/// <summary>
/// Defines a handler for discovering sub-types for polymorphic OpenAPI schemas.
/// </summary>
public interface ISubTypesHandler
{
/// <summary>
/// Determines whether this handler can discover sub-types for the specified type and document.
/// </summary>
/// <param name="type">The type to check.</param>
/// <param name="documentName">The OpenAPI document name.</param>
/// <returns><c>true</c> if this handler can handle the type; otherwise, <c>false</c>.</returns>
bool CanHandle(Type type, string documentName);
/// <summary>
/// Discovers sub-types for the specified type.
/// </summary>
/// <param name="type">The type to discover sub-types for.</param>
/// <returns>An enumerable of discovered sub-types.</returns>
IEnumerable<Type> Handle(Type type);
}
@@ -1,14 +0,0 @@
namespace Umbraco.Cms.Api.Common.OpenApi;
/// <summary>
/// Defines a selector for choosing sub-types from registered handlers.
/// </summary>
public interface ISubTypesSelector
{
/// <summary>
/// Selects sub-types for the specified type for polymorphic OpenAPI schema generation.
/// </summary>
/// <param name="type">The type to find sub-types for.</param>
/// <returns>An enumerable of sub-types.</returns>
IEnumerable<Type> SubTypes(Type type);
}
@@ -1,60 +0,0 @@
using Microsoft.OpenApi;
using Swashbuckle.AspNetCore.SwaggerGen;
using Umbraco.Extensions;
namespace Umbraco.Cms.Api.Common.OpenApi;
/// <summary>
/// This filter explicitly removes all other mime types than application/json from a named OpenAPI document when application/json is accepted.
/// </summary>
public class MimeTypeDocumentFilter : IDocumentFilter
{
private readonly string _documentName;
/// <summary>
/// Initializes a new instance of the <see cref="MimeTypeDocumentFilter"/> class.
/// </summary>
/// <param name="documentName">The name of the OpenAPI document to filter.</param>
public MimeTypeDocumentFilter(string documentName) => _documentName = documentName;
/// <inheritdoc/>
public void Apply(OpenApiDocument swaggerDoc, DocumentFilterContext context)
{
if (context.DocumentName != _documentName)
{
return;
}
OpenApiOperation[] operations = swaggerDoc.Paths
.SelectMany(path => path.Value.Operations?.Values ?? Enumerable.Empty<OpenApiOperation>())
.ToArray();
static void RemoveUnwantedMimeTypes(IDictionary<string, OpenApiMediaType>? content)
{
if (content is null || content.ContainsKey("application/json") is false)
{
return;
}
content.RemoveAll(r => r.Key != "application/json");
}
OpenApiRequestBody[] requestBodies = operations
.Select(operation => operation.RequestBody)
.OfType<OpenApiRequestBody>()
.ToArray();
foreach (OpenApiRequestBody requestBody in requestBodies)
{
RemoveUnwantedMimeTypes(requestBody.Content);
}
OpenApiResponse[] responses = operations
.SelectMany(operation => operation.Responses?.Values ?? Enumerable.Empty<IOpenApiResponse>())
.OfType<OpenApiResponse>()
.ToArray();
foreach (OpenApiResponse response in responses)
{
RemoveUnwantedMimeTypes(response.Content);
}
}
}
@@ -0,0 +1,68 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Formatters;
using Microsoft.AspNetCore.OpenApi;
using Microsoft.OpenApi;
using Umbraco.Extensions;
namespace Umbraco.Cms.Api.Common.OpenApi;
/// <summary>
/// Removes unwanted MIME types from OpenAPI operations, keeping only the content types
/// declared by <c>[Consumes]</c> for request bodies or <c>application/json</c> as the default.
/// </summary>
internal class MimeTypesTransformer : IOpenApiOperationTransformer
{
/// <inheritdoc/>
public Task TransformAsync(
OpenApiOperation operation,
OpenApiOperationTransformerContext context,
CancellationToken cancellationToken)
{
// For request bodies, keep only the content types declared in [Consumes], or fall back to application/json.
if (operation.RequestBody?.Content is { } requestContent)
{
var explicitContentTypes = context.Description.ActionDescriptor.EndpointMetadata
.OfType<ConsumesAttribute>()
.SelectMany(p => p.ContentTypes)
.Distinct()
.ToArray();
if (explicitContentTypes.Length != 0)
{
// Replace content types entirely with what [Consumes] declares,
// preserving the schema from the existing entry.
OpenApiMediaType? existingMediaType = requestContent.Values.FirstOrDefault();
requestContent.Clear();
foreach (var contentType in explicitContentTypes)
{
requestContent[contentType] = existingMediaType ?? new OpenApiMediaType();
}
}
else
{
RemoveNonJsonMimeTypes(requestContent);
}
}
// For responses, always keep only application/json.
foreach (IOpenApiResponse response in (operation.Responses ?? []).Values)
{
if (response is OpenApiResponse openApiResponse)
{
RemoveNonJsonMimeTypes(openApiResponse.Content);
}
}
return Task.CompletedTask;
}
private static void RemoveNonJsonMimeTypes(IDictionary<string, OpenApiMediaType>? content)
{
if (content?.ContainsKey("application/json") != true)
{
return;
}
content.RemoveAll(r => r.Key != "application/json");
}
}
@@ -0,0 +1,57 @@
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using Swashbuckle.AspNetCore.SwaggerUI;
using Umbraco.Cms.Core;
using Umbraco.Cms.Web.Common.ApplicationBuilder;
namespace Umbraco.Cms.Api.Common.OpenApi;
internal class OpenApiRouteTemplatePipelineFilter : UmbracoPipelineFilter
{
public OpenApiRouteTemplatePipelineFilter(string name)
: base(name)
{
PostPipeline = PostPipelineAction;
PreMapEndpoints = OnPreMapEndpointsAction;
}
private static void PostPipelineAction(IApplicationBuilder applicationBuilder)
{
UmbracoOpenApiOptions options = applicationBuilder.ApplicationServices
.GetRequiredService<IOptions<UmbracoOpenApiOptions>>().Value;
if (options.Enabled is false || options.DefaultUiEnabled is false)
{
return;
}
applicationBuilder.UseSwaggerUI(swaggerUiOptions => ConfigureSwaggerUi(swaggerUiOptions, options));
}
private static void OnPreMapEndpointsAction(IEndpointRouteBuilder endpoints)
{
UmbracoOpenApiOptions options = endpoints.ServiceProvider
.GetRequiredService<IOptions<UmbracoOpenApiOptions>>().Value;
if (options.Enabled is false)
{
return;
}
endpoints.MapOpenApi(options.RouteTemplate);
}
private static void ConfigureSwaggerUi(SwaggerUIOptions swaggerUiOptions, UmbracoOpenApiOptions options)
{
swaggerUiOptions.RoutePrefix = options.UiRoutePrefix;
// Add custom configuration from https://swagger.io/docs/open-source-tools/swagger-ui/usage/configuration/
swaggerUiOptions.ConfigObject.PersistAuthorization = true; // persists authorization data so it would not be lost on browser close/refresh
swaggerUiOptions.ConfigObject.Filter = string.Empty; // Enable the filter with an empty string as default filter.
swaggerUiOptions.OAuthClientId(Constants.OAuthClientIds.OpenApiUi);
swaggerUiOptions.OAuthUsePkce();
}
}
@@ -1,35 +0,0 @@
using Asp.Versioning;
using Microsoft.AspNetCore.Mvc.ApiExplorer;
namespace Umbraco.Cms.Api.Common.OpenApi;
/// <summary>
/// Selects an operation ID for an API description using registered handlers.
/// </summary>
public class OperationIdSelector : IOperationIdSelector
{
private readonly IEnumerable<IOperationIdHandler> _operationIdHandlers;
/// <summary>
/// Initializes a new instance of the <see cref="OperationIdSelector"/> class.
/// </summary>
[Obsolete("Use non-obsolete constructor. Scheduled for removal in Umbraco 18.")]
public OperationIdSelector()
: this(Enumerable.Empty<IOperationIdHandler>())
{
}
/// <summary>
/// Initializes a new instance of the <see cref="OperationIdSelector"/> class.
/// </summary>
/// <param name="operationIdHandlers">The registered operation ID handlers.</param>
public OperationIdSelector(IEnumerable<IOperationIdHandler> operationIdHandlers)
=> _operationIdHandlers = operationIdHandlers;
/// <inheritdoc/>
public virtual string? OperationId(ApiDescription apiDescription)
{
IOperationIdHandler? handler = _operationIdHandlers.FirstOrDefault(h => h.CanHandle(apiDescription));
return handler?.Handle(apiDescription);
}
}
@@ -1,30 +0,0 @@
using Microsoft.OpenApi;
using Swashbuckle.AspNetCore.SwaggerGen;
namespace Umbraco.Cms.Api.Common.OpenApi;
/// <summary>
/// This filter explicitly removes all security schemes from a named OpenAPI document.
/// </summary>
public class RemoveSecuritySchemesDocumentFilter : IDocumentFilter
{
private readonly string _documentName;
/// <summary>
/// Initializes a new instance of the <see cref="RemoveSecuritySchemesDocumentFilter"/> class.
/// </summary>
/// <param name="documentName">The name of the OpenAPI document to filter.</param>
public RemoveSecuritySchemesDocumentFilter(string documentName)
=> _documentName = documentName;
/// <inheritdoc/>
public void Apply(OpenApiDocument swaggerDoc, DocumentFilterContext context)
{
if (context.DocumentName != _documentName)
{
return;
}
swaggerDoc.Components?.SecuritySchemes?.Clear();
}
}
@@ -0,0 +1,48 @@
using System.Reflection;
using System.Text.Json.Serialization.Metadata;
using Microsoft.AspNetCore.OpenApi;
using Microsoft.OpenApi;
namespace Umbraco.Cms.Api.Common.OpenApi;
/// <summary>
/// Ensures that all non-nullable properties are marked as required in the OpenAPI schema.
/// </summary>
/// <remarks>By default, only properties marked with the required keyword will actually show as required.
/// Non-nullable reference types were not taken into account.</remarks>
internal class RequireNonNullablePropertiesSchemaTransformer : IOpenApiSchemaTransformer
{
/// <inheritdoc />
public Task TransformAsync(OpenApiSchema schema, OpenApiSchemaTransformerContext context, CancellationToken cancellationToken)
{
IEnumerable<string> additionalRequiredProps = schema.Properties?
.Where(p => schema.Required?.Contains(p.Key) != true) // If it's already required, skip
.Where(x => IsRequiredProperty(schema, context.JsonTypeInfo, x.Key))
.Select(x => x.Key)
?? [];
schema.Required ??= new HashSet<string>();
foreach (var propKey in additionalRequiredProps)
{
schema.Required.Add(propKey);
}
return Task.CompletedTask;
}
private static bool IsRequiredProperty(OpenApiSchema schema, JsonTypeInfo jsonTypeInfo, string propertyName)
{
if (jsonTypeInfo.Properties.FirstOrDefault(p => p.Name == propertyName) is { } property)
{
return property.IsGetNullable is false;
}
// If we can't find the property in the type (e.g. discriminator '$type'), use the schema type information.
if (schema.Properties?.TryGetValue(propertyName, out IOpenApiSchema? schemaProperty) is true
&& schemaProperty?.Type is { } propertyType)
{
return propertyType.HasFlag(JsonSchemaType.Null) is false;
}
return false;
}
}
@@ -1,23 +0,0 @@
namespace Umbraco.Cms.Api.Common.OpenApi;
/// <summary>
/// Selects a schema ID for a type using registered handlers.
/// </summary>
public class SchemaIdSelector : ISchemaIdSelector
{
private readonly IEnumerable<ISchemaIdHandler> _schemaIdHandlers;
/// <summary>
/// Initializes a new instance of the <see cref="SchemaIdSelector"/> class.
/// </summary>
/// <param name="schemaIdHandlers">The registered schema ID handlers.</param>
public SchemaIdSelector(IEnumerable<ISchemaIdHandler> schemaIdHandlers)
=> _schemaIdHandlers = schemaIdHandlers;
/// <inheritdoc/>
public virtual string SchemaId(Type type)
{
ISchemaIdHandler? handler = _schemaIdHandlers.FirstOrDefault(h => h.CanHandle(type));
return handler?.Handle(type) ?? type.Name;
}
}
@@ -0,0 +1,42 @@
using Microsoft.AspNetCore.OpenApi;
using Microsoft.OpenApi;
namespace Umbraco.Cms.Api.Common.OpenApi;
/// <summary>
/// Transforms the OpenAPI document to sort tags and paths alphabetically.
/// </summary>
internal class SortTagsAndPathsTransformer : IOpenApiDocumentTransformer
{
/// <summary>
/// Transforms the specified OpenAPI document to sort its tags and paths alphabetically.
/// </summary>
/// <param name="document">The <see cref="OpenApiDocument"/> to modify.</param>
/// <param name="context">The <see cref="OpenApiDocumentTransformerContext"/> associated with the <paramref name="document"/>.</param>
/// <param name="cancellationToken">The cancellation token to use.</param>
/// <returns>The task object representing the asynchronous operation.</returns>
public Task TransformAsync(
OpenApiDocument document,
OpenApiDocumentTransformerContext context,
CancellationToken cancellationToken)
{
document.Tags = new SortedSet<OpenApiTag>(
document.Tags ?? Enumerable.Empty<OpenApiTag>(),
Comparer<OpenApiTag>.Create((a, b) => string.Compare(a.Name, b.Name, StringComparison.Ordinal)));
var sortedPaths = new OpenApiPaths();
foreach (KeyValuePair<string, IOpenApiPathItem> keyValuePair in document.Paths
.OrderBy(x => x.Value.Operations?.Values
.SelectMany(op => op.Tags ?? Enumerable.Empty<OpenApiTagReference>())
.OrderBy(t => t.Name)
.FirstOrDefault()?
.Name)
.ThenBy(x => x.Key))
{
sortedPaths.Add(keyValuePair.Key, keyValuePair.Value);
}
document.Paths = sortedPaths;
return Task.CompletedTask;
}
}
@@ -1,34 +0,0 @@
using Umbraco.Cms.Api.Common.Serialization;
namespace Umbraco.Cms.Api.Common.OpenApi;
/// <summary>
/// Default handler for discovering sub-types for polymorphic OpenAPI schemas.
/// </summary>
public class SubTypesHandler : ISubTypesHandler
{
private readonly IUmbracoJsonTypeInfoResolver _umbracoJsonTypeInfoResolver;
/// <summary>
/// Initializes a new instance of the <see cref="SubTypesHandler"/> class.
/// </summary>
/// <param name="umbracoJsonTypeInfoResolver">The JSON type info resolver for finding sub-types.</param>
public SubTypesHandler(IUmbracoJsonTypeInfoResolver umbracoJsonTypeInfoResolver)
=> _umbracoJsonTypeInfoResolver = umbracoJsonTypeInfoResolver;
/// <summary>
/// Determines whether this handler can process the specified type based on namespace.
/// </summary>
/// <param name="type">The type to check.</param>
/// <returns><c>true</c> if the type is in an Umbraco.Cms namespace; otherwise, <c>false</c>.</returns>
protected virtual bool CanHandle(Type type)
=> type.Namespace?.StartsWith("Umbraco.Cms") is true;
/// <inheritdoc/>
public virtual bool CanHandle(Type type, string documentName)
=> CanHandle(type);
/// <inheritdoc/>
public virtual IEnumerable<Type> Handle(Type type)
=> _umbracoJsonTypeInfoResolver.FindSubTypes(type);
}
@@ -1,67 +0,0 @@
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Options;
using Umbraco.Cms.Api.Common.Serialization;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Configuration.Models;
using Umbraco.Cms.Core.Hosting;
using Umbraco.Extensions;
namespace Umbraco.Cms.Api.Common.OpenApi;
/// <summary>
/// Selects sub-types for polymorphic OpenAPI schemas using registered handlers.
/// </summary>
public class SubTypesSelector : ISubTypesSelector
{
private readonly IHostingEnvironment _hostingEnvironment;
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly IEnumerable<ISubTypesHandler> _subTypeHandlers;
private readonly IUmbracoJsonTypeInfoResolver _umbracoJsonTypeInfoResolver;
/// <summary>
/// Initializes a new instance of the <see cref="SubTypesSelector"/> class.
/// </summary>
/// <param name="hostingEnvironment">The hosting environment.</param>
/// <param name="httpContextAccessor">The HTTP context accessor.</param>
/// <param name="subTypeHandlers">The registered sub-type handlers.</param>
/// <param name="umbracoJsonTypeInfoResolver">The JSON type info resolver for finding sub-types.</param>
public SubTypesSelector(
IHostingEnvironment hostingEnvironment,
IHttpContextAccessor httpContextAccessor,
IEnumerable<ISubTypesHandler> subTypeHandlers,
IUmbracoJsonTypeInfoResolver umbracoJsonTypeInfoResolver)
{
_hostingEnvironment = hostingEnvironment;
_httpContextAccessor = httpContextAccessor;
_subTypeHandlers = subTypeHandlers;
_umbracoJsonTypeInfoResolver = umbracoJsonTypeInfoResolver;
}
/// <inheritdoc/>
public IEnumerable<Type> SubTypes(Type type)
{
var backOfficePath = _hostingEnvironment.GetBackOfficePath();
var swaggerPath = $"{backOfficePath}/swagger";
if (_httpContextAccessor.HttpContext?.Request.Path.StartsWithSegments(swaggerPath) ?? false)
{
// Split the path into segments
var segments = _httpContextAccessor.HttpContext.Request.Path.Value![swaggerPath.Length..]
.TrimStart(Constants.CharArrays.ForwardSlash)
.Split(Constants.CharArrays.ForwardSlash);
// Extract the document name from the path
var documentName = segments[0];
// Find the first handler that can handle the type / document name combination
ISubTypesHandler? handler = _subTypeHandlers.FirstOrDefault(h => h.CanHandle(type, documentName));
if (handler != null)
{
return handler.Handle(type);
}
}
// Default implementation to maintain backwards compatibility
return _umbracoJsonTypeInfoResolver.FindSubTypes(type);
}
}
@@ -1,98 +0,0 @@
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Options;
using Microsoft.OpenApi;
using Swashbuckle.AspNetCore.SwaggerGen;
using Swashbuckle.AspNetCore.SwaggerUI;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Hosting;
using Umbraco.Cms.Web.Common.ApplicationBuilder;
using IHostingEnvironment = Umbraco.Cms.Core.Hosting.IHostingEnvironment;
namespace Umbraco.Cms.Api.Common.OpenApi;
/// <summary>
/// Pipeline filter that configures Swagger/OpenAPI endpoints for Umbraco APIs.
/// </summary>
public class SwaggerRouteTemplatePipelineFilter : UmbracoPipelineFilter
{
/// <summary>
/// Initializes a new instance of the <see cref="SwaggerRouteTemplatePipelineFilter"/> class.
/// </summary>
/// <param name="name">The name of the pipeline filter.</param>
public SwaggerRouteTemplatePipelineFilter(string name)
: base(name)
=> PostPipeline = PostPipelineAction;
private void PostPipelineAction(IApplicationBuilder applicationBuilder)
{
if (SwaggerIsEnabled(applicationBuilder) is false)
{
return;
}
IOptions<SwaggerGenOptions> swaggerGenOptions = applicationBuilder.ApplicationServices.GetRequiredService<IOptions<SwaggerGenOptions>>();
applicationBuilder.UseSwagger(swaggerOptions =>
{
swaggerOptions.RouteTemplate = SwaggerRouteTemplate(applicationBuilder);
});
applicationBuilder.UseSwaggerUI(swaggerUiOptions => SwaggerUiConfiguration(swaggerUiOptions, swaggerGenOptions.Value, applicationBuilder));
}
/// <summary>
/// Determines whether Swagger is enabled for the application.
/// </summary>
/// <param name="applicationBuilder">The application builder.</param>
/// <returns><c>true</c> if Swagger is enabled; otherwise, <c>false</c>.</returns>
protected virtual bool SwaggerIsEnabled(IApplicationBuilder applicationBuilder)
=> applicationBuilder.ApplicationServices.GetRequiredService<IWebHostEnvironment>().IsProduction() is false;
/// <summary>
/// Gets the route template for Swagger JSON endpoints.
/// </summary>
/// <param name="applicationBuilder">The application builder.</param>
/// <returns>The Swagger route template.</returns>
protected virtual string SwaggerRouteTemplate(IApplicationBuilder applicationBuilder)
=> $"{GetBackOfficePath(applicationBuilder).TrimStart(Constants.CharArrays.ForwardSlash)}/swagger/{{documentName}}/swagger.json";
/// <summary>
/// Gets the route prefix for the Swagger UI.
/// </summary>
/// <param name="applicationBuilder">The application builder.</param>
/// <returns>The Swagger UI route prefix.</returns>
protected virtual string SwaggerUiRoutePrefix(IApplicationBuilder applicationBuilder)
=> $"{GetBackOfficePath(applicationBuilder).TrimStart(Constants.CharArrays.ForwardSlash)}/swagger";
/// <summary>
/// Configures the Swagger UI options.
/// </summary>
/// <param name="swaggerUiOptions">The Swagger UI options to configure.</param>
/// <param name="swaggerGenOptions">The Swagger generation options.</param>
/// <param name="applicationBuilder">The application builder.</param>
protected virtual void SwaggerUiConfiguration(
SwaggerUIOptions swaggerUiOptions,
SwaggerGenOptions swaggerGenOptions,
IApplicationBuilder applicationBuilder)
{
swaggerUiOptions.RoutePrefix = SwaggerUiRoutePrefix(applicationBuilder);
foreach ((var name, OpenApiInfo? apiInfo) in swaggerGenOptions.SwaggerGeneratorOptions.SwaggerDocs.OrderBy(x => x.Value.Title))
{
swaggerUiOptions.SwaggerEndpoint($"{name}/swagger.json", $"{apiInfo.Title}");
}
// Add custom configuration from https://swagger.io/docs/open-source-tools/swagger-ui/usage/configuration/
swaggerUiOptions.ConfigObject.PersistAuthorization = true; // persists authorization data so it would not be lost on browser close/refresh
swaggerUiOptions.ConfigObject.Filter = string.Empty; // Enable the filter with an empty string as default filter.
swaggerUiOptions.OAuthClientId(Constants.OAuthClientIds.Swagger);
swaggerUiOptions.OAuthUsePkce();
}
private string GetBackOfficePath(IApplicationBuilder applicationBuilder)
=> applicationBuilder.ApplicationServices.GetRequiredService<IHostingEnvironment>().GetBackOfficePath();
}
@@ -0,0 +1,67 @@
using Microsoft.AspNetCore.OpenApi;
using Microsoft.OpenApi;
namespace Umbraco.Cms.Api.Common.OpenApi;
/// <summary>
/// Transformer that tags operations based on their group name.
/// </summary>
internal class TagActionsByGroupNameTransformer : IOpenApiOperationTransformer, IOpenApiDocumentTransformer
{
/// <summary>
/// Transforms the specified OpenAPI operation in order to tag it by its group name.
/// </summary>
/// <param name="operation">The <see cref="OpenApiOperation"/> to modify.</param>
/// <param name="context">The <see cref="OpenApiOperationTransformerContext"/> associated with the <paramref name="operation"/>.</param>
/// <param name="cancellationToken">The cancellation token to use.</param>
/// <returns>The task object representing the asynchronous operation.</returns>
public Task TransformAsync(
OpenApiOperation operation,
OpenApiOperationTransformerContext context,
CancellationToken cancellationToken)
{
if (context.Document is null || context.Description.GroupName is not { } groupName)
{
return Task.CompletedTask;
}
operation.Tags = new HashSet<OpenApiTagReference> { new(groupName) };
if (context.Document.Tags?.Any(t => t.Name == groupName) == true)
{
return Task.CompletedTask;
}
context.Document.Tags ??= new HashSet<OpenApiTag>();
context.Document.Tags.Add(new OpenApiTag { Name = groupName });
return Task.CompletedTask;
}
/// <summary>
/// Transforms the specified OpenAPI document in order to clean up unused tags.
/// </summary>
/// <param name="document">The <see cref="OpenApiDocument"/> to modify.</param>
/// <param name="context">The <see cref="OpenApiDocumentTransformerContext"/> associated with the <paramref name="document"/>.</param>
/// <param name="cancellationToken">The cancellation token to use.</param>
/// <returns>The task object representing the asynchronous operation.</returns>
public Task TransformAsync(
OpenApiDocument document,
OpenApiDocumentTransformerContext context,
CancellationToken cancellationToken)
{
var usedTags = new HashSet<string?>(document.Paths
.SelectMany(p => (p.Value.Operations ?? []).Values)
.SelectMany(o => o.Tags ?? new HashSet<OpenApiTagReference>())
.Select(t => t.Name));
var tagsToRemove = (document.Tags ?? Enumerable.Empty<OpenApiTag>())
.Where(tag => usedTags.Contains(tag.Name) is false)
.ToList();
foreach (OpenApiTag tag in tagsToRemove)
{
document.Tags?.Remove(tag);
}
return Task.CompletedTask;
}
}
@@ -0,0 +1,48 @@
namespace Umbraco.Cms.Api.Common.OpenApi;
/// <summary>
/// Options for configuring OpenAPI documents and UI.
/// </summary>
/// <remarks>
/// These options are populated by <c>AddUmbracoOpenApi</c> during DI configuration, which resolves the back-office path
/// from <see cref="Core.Hosting.IHostingEnvironment"/> and sets the default values for
/// <see cref="RouteTemplate"/> and <see cref="UiRoutePrefix"/>. Consumers that read this options type before
/// <c>AddUmbracoOpenApi</c> has run will observe the uninitialised defaults (empty strings for the route properties).
/// </remarks>
public class UmbracoOpenApiOptions
{
/// <summary>
/// Gets or sets whether OpenAPI documents are enabled.
/// Configured to <c>true</c> in non-production environments by default; <c>false</c> until configured.
/// This avoids exposing API structure on public-facing websites.
/// </summary>
public bool Enabled { get; set; }
/// <summary>
/// Gets or sets whether the default OpenAPI UI is enabled.
/// Only applies when <see cref="Enabled"/> is true.
/// Set to false to disable the default UI while keeping OpenAPI documents available,
/// allowing you to use an alternative UI.
/// Default: true.
/// </summary>
public bool DefaultUiEnabled { get; set; } = true;
/// <summary>
/// Gets or sets the route template for OpenAPI JSON documents.
/// Use <c>{documentName}</c> as a placeholder for the document name.
/// </summary>
/// <remarks>
/// Populated by <c>AddUmbracoOpenApi</c> to <c>"{backOfficePath}/openapi/{documentName}.json"</c>. The initial
/// <see cref="string.Empty"/> default is a sentinel for "not yet configured" — it is not a usable route template.
/// </remarks>
public string RouteTemplate { get; set; } = string.Empty;
/// <summary>
/// Gets or sets the route prefix for OpenAPI UI.
/// </summary>
/// <remarks>
/// Populated by <c>AddUmbracoOpenApi</c> to <c>"{backOfficePath}/openapi"</c>. The initial <see cref="string.Empty"/>
/// default is a sentinel for "not yet configured" — it is not a usable route prefix.
/// </remarks>
public string UiRoutePrefix { get; set; } = string.Empty;
}
@@ -1,63 +1,47 @@
using Asp.Versioning;
using Microsoft.AspNetCore.Mvc.ApiExplorer;
using Microsoft.AspNetCore.Mvc.Controllers;
using Microsoft.AspNetCore.OpenApi;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using Microsoft.OpenApi;
using Umbraco.Extensions;
namespace Umbraco.Cms.Api.Common.OpenApi;
/// <summary>
/// Default handler for generating OpenAPI operation IDs for Umbraco API controllers.
/// Transforms OpenAPI operation IDs using Umbraco's naming conventions.
/// </summary>
/// <remarks>
/// Left unsealed on purpose, so it is extendable by consuming APIs.
/// This transformer can be registered manually for custom OpenAPI configurations.
/// </remarks>
public class OperationIdHandler : IOperationIdHandler
public class UmbracoOperationIdTransformer : IOpenApiOperationTransformer
{
private readonly ApiVersioningOptions _apiVersioningOptions;
/// <summary>
/// Initializes a new instance of the <see cref="OperationIdHandler"/> class.
/// Transforms the specified OpenAPI operation, setting its operation ID using a custom selector.
/// </summary>
/// <param name="apiVersioningOptions">The API versioning options.</param>
public OperationIdHandler(IOptions<ApiVersioningOptions> apiVersioningOptions)
=> _apiVersioningOptions = apiVersioningOptions.Value;
/// <inheritdoc/>
public bool CanHandle(ApiDescription apiDescription)
/// <param name="operation">The <see cref="OpenApiOperation"/> to modify.</param>
/// <param name="context">The <see cref="OpenApiOperationTransformerContext"/> associated with the <paramref name="operation"/>.</param>
/// <param name="cancellationToken">The cancellation token to use.</param>
/// <returns>The task object representing the asynchronous operation.</returns>
public Task TransformAsync(
OpenApiOperation operation,
OpenApiOperationTransformerContext context,
CancellationToken cancellationToken)
{
if (apiDescription.ActionDescriptor is not ControllerActionDescriptor controllerActionDescriptor)
{
return false;
}
return CanHandle(apiDescription, controllerActionDescriptor);
operation.OperationId = GenerateOperationId(context);
return Task.CompletedTask;
}
/// <summary>
/// Determines whether this handler can process the API description based on the controller namespace.
/// </summary>
/// <param name="apiDescription">The API description.</param>
/// <param name="controllerActionDescriptor">The controller action descriptor.</param>
/// <returns><c>true</c> if the controller is in an Umbraco.Cms.Api namespace; otherwise, <c>false</c>.</returns>
protected virtual bool CanHandle(ApiDescription apiDescription, ControllerActionDescriptor controllerActionDescriptor)
=> controllerActionDescriptor.ControllerTypeInfo.Namespace?.StartsWith("Umbraco.Cms.Api") is true;
/// <inheritdoc/>
public virtual string Handle(ApiDescription apiDescription)
=> UmbracoOperationId(apiDescription);
/// <summary>
/// Generates a unique operation identifier for a given API following Umbraco's operation id naming conventions.
/// </summary>
protected string UmbracoOperationId(ApiDescription apiDescription)
private static string GenerateOperationId(OpenApiOperationTransformerContext context)
{
ApiDescription apiDescription = context.Description;
if (apiDescription.ActionDescriptor is not ControllerActionDescriptor controllerActionDescriptor)
{
throw new ArgumentException($"This handler operates only on {nameof(ControllerActionDescriptor)}.");
}
ApiVersion defaultVersion = _apiVersioningOptions.DefaultApiVersion;
ApiVersion defaultVersion = context.ApplicationServices.GetRequiredService<IOptions<ApiVersioningOptions>>().Value.DefaultApiVersion;
var httpMethod = apiDescription.HttpMethod?.ToLower().ToFirstUpper() ?? "Get";
// if the route info "Name" is supplied we'll use this explicitly as the operation ID
@@ -4,29 +4,18 @@ using Umbraco.Extensions;
namespace Umbraco.Cms.Api.Common.OpenApi;
/// <summary>
/// Default handler for generating OpenAPI schema IDs for Umbraco types.
/// Static utility for generating OpenAPI schema IDs following Umbraco's naming conventions.
/// </summary>
/// <remarks>
/// Left unsealed on purpose, so it is extendable by consuming APIs.
/// Adds "Model" suffix to avoid TypeScript name clashes and removes invalid characters.
/// </remarks>
public class SchemaIdHandler : ISchemaIdHandler
public static class UmbracoSchemaIdGenerator
{
/// <inheritdoc/>
public virtual bool CanHandle(Type type)
=> type.Namespace?.StartsWith("Umbraco.Cms") is true;
/// <inheritdoc/>
public virtual string Handle(Type type)
=> UmbracoSchemaId(type);
/// <summary>
/// Generates a sanitized and consistent schema identifier for a given type following Umbraco's schema id naming conventions.
/// Generates a schema ID for the specified type following Umbraco's naming conventions.
/// </summary>
protected string UmbracoSchemaId(Type type)
/// <param name="type">The type to generate a schema ID for.</param>
/// <returns>The generated schema ID.</returns>
public static string Generate(Type type)
{
var name = SanitizedTypeName(type);
name = HandleGenerics(name, type);
if (name.EndsWith("Model") == false)
@@ -40,13 +29,13 @@ public class SchemaIdHandler : ISchemaIdHandler
return Regex.Replace(name, @"[^\w]", string.Empty);
}
private string SanitizedTypeName(Type t) => t.Name
private static string SanitizedTypeName(Type t) => t.Name
// first grab the "non-generic" part of any generic type name (i.e. "PagedViewModel`1" becomes "PagedViewModel")
.Split('`').First()
// then remove the "ViewModel" postfix from type names
.TrimEnd("ViewModel");
private string HandleGenerics(string name, Type type)
private static string HandleGenerics(string name, Type type)
{
if (!type.IsGenericType)
{
@@ -3,6 +3,7 @@ using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Options;
using OpenIddict.Server;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Configuration.Models;
using Umbraco.Cms.Core.Security;
using Umbraco.Extensions;
@@ -32,12 +33,12 @@ public class ExposeBackOfficeAuthenticationOpenIddictServerEventsHandler : IOpen
// These are the type identifiers for the claims required by the principal
// for the custom authentication scheme.
// We make available the ID, user name and allowed applications (sections) claims.
// We make available the ID and user name claims, plus the claim necessary for parsing the user key.
_claimTypes =
[
backOfficeIdentityOptions.Value.ClaimsIdentity.UserIdClaimType,
backOfficeIdentityOptions.Value.ClaimsIdentity.UserNameClaimType,
Core.Constants.Security.AllowedApplicationsClaimType,
Constants.Security.OpenIdDictSubClaimType
];
}
@@ -8,6 +8,12 @@
<AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleTo">
<_Parameter1>Umbraco.Tests.UnitTests</_Parameter1>
</AssemblyAttribute>
<AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleTo">
<_Parameter1>Umbraco.Cms.Api.Management</_Parameter1>
</AssemblyAttribute>
<AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleTo">
<_Parameter1>Umbraco.Cms.Api.Delivery</_Parameter1>
</AssemblyAttribute>
</ItemGroup>
<ItemGroup>
@@ -15,11 +21,12 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Asp.Versioning.Mvc "/>
<PackageReference Include="Asp.Versioning.Mvc" />
<PackageReference Include="Asp.Versioning.Mvc.ApiExplorer" />
<PackageReference Include="Swashbuckle.AspNetCore" />
<PackageReference Include="Microsoft.AspNetCore.OpenApi" />
<PackageReference Include="OpenIddict.Abstractions" />
<PackageReference Include="OpenIddict.AspNetCore" />
<PackageReference Include="Swashbuckle.AspNetCore.SwaggerUI" />
</ItemGroup>
<ItemGroup>
+2 -3
View File
@@ -39,7 +39,7 @@ Umbraco.Cms.Api.Delivery/
├── Services/ # Business logic and query building
├── Caching/ # Output cache policies
├── Rendering/ # Output expansion strategies
├── Configuration/ # Swagger configuration
├── Configuration/ # OpenAPI configuration
└── Filters/ # Action filters (access, validation)
```
@@ -200,10 +200,9 @@ context.EnableOutputCaching = requestPreviewService.IsPreview() is false
### Technical Debt (TODOs in codebase)
1. **V1 Removal Pending** (4 locations):
1. **V1 Removal Pending** (2 locations):
- `DependencyInjection/UmbracoBuilderExtensions.cs:98` - FIXME: remove matcher policy
- `Routing/DeliveryApiItemsEndpointsMatcherPolicy.cs:11` - FIXME: remove class
- `Filters/SwaggerDocumentationFilterBase.cs:79,83` - FIXME: remove V1 swagger docs
2. **Obsolete Reference Warnings** (csproj:9-13):
- `ASP0019` - IHeaderDictionary.Append usage
@@ -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,67 @@
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="ElementCacheRefresherNotification"/> to evict Delivery API output cache entries
/// for content that references the changed element via picker properties (umbElement relations).
/// </summary>
internal sealed class DeliveryApiElementOutputCacheEvictionHandler
: RelationOutputCacheEvictionHandlerBase, INotificationAsyncHandler<ElementCacheRefresherNotification>
{
private readonly ILogger<DeliveryApiElementOutputCacheEvictionHandler> _logger;
/// <summary>
/// Initializes a new instance of the <see cref="DeliveryApiElementOutputCacheEvictionHandler"/> 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 DeliveryApiElementOutputCacheEvictionHandler(
IOutputCacheStore outputCacheStore,
IRelationService relationService,
IIdKeyMap idKeyMap,
ILogger<DeliveryApiElementOutputCacheEvictionHandler> logger)
: base(outputCacheStore, relationService, idKeyMap)
=> _logger = logger;
/// <inheritdoc />
public async Task HandleAsync(ElementCacheRefresherNotification notification, CancellationToken cancellationToken)
{
if (notification.MessageType != MessageType.RefreshByPayload
|| notification.MessageObject is not ElementCacheRefresher.JsonPayload[] payloads)
{
return;
}
foreach (ElementCacheRefresher.JsonPayload payload in payloads)
{
if (payload.ChangeTypes.HasFlag(TreeChangeTypes.RefreshAll))
{
// Evict all Delivery API responses — content responses may include referenced elements,
// so evicting only element-related entries would leave stale element references in content responses.
_logger.LogDebug("Element refresh all — evicting all Delivery API output cache entries.");
await OutputCacheStore.EvictByTagAsync(Constants.DeliveryApi.OutputCache.AllTag, cancellationToken);
return;
}
}
// Evict content that references the changed elements via picker properties.
await EvictRelatedContentAsync(
payloads.Select(p => p.Id),
Constants.Conventions.RelationTypes.RelatedElementAlias,
Constants.DeliveryApi.OutputCache.ContentTagPrefix,
_logger,
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();
}
@@ -0,0 +1,62 @@
using Microsoft.AspNetCore.OpenApi;
using Microsoft.Extensions.Options;
using Umbraco.Cms.Api.Common.Configuration;
using Umbraco.Cms.Api.Common.OpenApi;
using Umbraco.Cms.Api.Delivery.OpenApi.Transformers;
using Umbraco.Cms.Core.Configuration.Models;
namespace Umbraco.Cms.Api.Delivery.Configuration;
/// <summary>
/// Configures the OpenAPI options for the Umbraco Delivery API.
/// </summary>
internal class ConfigureUmbracoDeliveryApiOpenApiOptions : ConfigureUmbracoOpenApiOptionsBase
{
private readonly DeliveryApiSettings _deliveryApiSettings;
/// <summary>
/// Initializes a new instance of the <see cref="ConfigureUmbracoDeliveryApiOpenApiOptions"/> class.
/// </summary>
/// <param name="deliveryApiSettings">The Delivery API settings.</param>
public ConfigureUmbracoDeliveryApiOpenApiOptions(IOptions<DeliveryApiSettings> deliveryApiSettings)
{
_deliveryApiSettings = deliveryApiSettings.Value;
}
/// <inheritdoc />
protected override string ApiName => DeliveryApiConfiguration.ApiName;
/// <inheritdoc />
protected override string ApiTitle => DeliveryApiConfiguration.ApiTitle;
/// <inheritdoc />
protected override string ApiVersion => "Latest";
/// <inheritdoc />
protected override string ApiDescription =>
$"You can find out more about the {DeliveryApiConfiguration.ApiTitle} in [the documentation]({DeliveryApiConfiguration.ApiDocumentationContentArticleLink}).";
/// <inheritdoc />
protected override void ConfigureOpenApi(OpenApiOptions options)
{
base.ConfigureOpenApi(options);
// Add API key security scheme and configure it for all operations
options
.AddDocumentTransformer<ApiKeyTransformer>()
.AddOperationTransformer<ApiKeyTransformer>();
options.AddSchemaTransformer<RequireNonNullablePropertiesSchemaTransformer>();
options.AddSchemaTransformer<FixFileReturnTypesTransformer>();
options.AddOperationTransformer<MimeTypesTransformer>();
options.AddOperationTransformer<ContentApiTransformer>();
options.AddOperationTransformer<MediaApiTransformer>();
if (_deliveryApiSettings.OpenApi.GenerateContentTypeSchemas)
{
options
.AddSchemaTransformer<ContentTypeSchemaTransformer>()
.AddDocumentTransformer<ContentTypeSchemaTransformer>();
}
}
}
@@ -1,31 +0,0 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using Microsoft.OpenApi;
using Swashbuckle.AspNetCore.SwaggerGen;
using Umbraco.Cms.Api.Common.OpenApi;
using Umbraco.Cms.Api.Delivery.Filters;
namespace Umbraco.Cms.Api.Delivery.Configuration;
public class ConfigureUmbracoDeliveryApiSwaggerGenOptions: IConfigureOptions<SwaggerGenOptions>
{
public void Configure(SwaggerGenOptions swaggerGenOptions)
{
swaggerGenOptions.SwaggerDoc(
DeliveryApiConfiguration.ApiName,
new OpenApiInfo
{
Title = DeliveryApiConfiguration.ApiTitle,
Version = "Latest",
Description = $"You can find out more about the {DeliveryApiConfiguration.ApiTitle} in [the documentation]({DeliveryApiConfiguration.ApiDocumentationContentArticleLink})."
});
swaggerGenOptions.DocumentFilter<MimeTypeDocumentFilter>(DeliveryApiConfiguration.ApiName);
swaggerGenOptions.DocumentFilter<RemoveSecuritySchemesDocumentFilter>(DeliveryApiConfiguration.ApiName);
swaggerGenOptions.OperationFilter<SwaggerContentDocumentationFilter>();
swaggerGenOptions.OperationFilter<SwaggerMediaDocumentationFilter>();
swaggerGenOptions.ParameterFilter<SwaggerContentDocumentationFilter>();
swaggerGenOptions.ParameterFilter<SwaggerMediaDocumentationFilter>();
}
}
@@ -0,0 +1,47 @@
using System.Text.Json.Serialization;
using Microsoft.AspNetCore.Http.Json;
using Microsoft.Extensions.Options;
using Umbraco.Cms.Core;
namespace Umbraco.Cms.Api.Delivery.Configuration;
/// <summary>
/// Configures the Http JSON options for the Umbraco Delivery API.
/// </summary>
internal class ConfigureUmbracoDeliveryHttpJsonOptions : IConfigureNamedOptions<JsonOptions>
{
private readonly IOptionsMonitor<Microsoft.AspNetCore.Mvc.JsonOptions> _mvcJsonOptions;
/// <summary>
/// Initializes a new instance of the <see cref="ConfigureUmbracoDeliveryHttpJsonOptions"/> class.
/// </summary>
/// <param name="mvcJsonOptions">The configured MVC json options.</param>
public ConfigureUmbracoDeliveryHttpJsonOptions(IOptionsMonitor<Microsoft.AspNetCore.Mvc.JsonOptions> mvcJsonOptions)
=> _mvcJsonOptions = mvcJsonOptions;
/// <inheritdoc />
public void Configure(JsonOptions options) => Configure(Options.DefaultName, options);
/// <inheritdoc />
public void Configure(string? name, JsonOptions options)
{
if (name != Constants.JsonOptionsNames.DeliveryApi)
{
return;
}
// Copy all converters from the Delivery API MVC JSON options
Microsoft.AspNetCore.Mvc.JsonOptions backofficeMvcJsonOptions = _mvcJsonOptions.Get(Constants.JsonOptionsNames.DeliveryApi);
foreach (JsonConverter jsonConverter in backofficeMvcJsonOptions.JsonSerializerOptions.Converters)
{
options.SerializerOptions.Converters.Add(jsonConverter);
}
options.SerializerOptions.PropertyNamingPolicy = backofficeMvcJsonOptions.JsonSerializerOptions.PropertyNamingPolicy;
options.SerializerOptions.TypeInfoResolver = backofficeMvcJsonOptions.JsonSerializerOptions.TypeInfoResolver;
options.SerializerOptions.MaxDepth = backofficeMvcJsonOptions.JsonSerializerOptions.MaxDepth;
// Open API specific settings
options.SerializerOptions.NumberHandling = JsonNumberHandling.Strict;
}
}
@@ -1,69 +0,0 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using Microsoft.OpenApi;
using Swashbuckle.AspNetCore.SwaggerGen;
using Umbraco.Cms.Api.Common.Security;
using Umbraco.Cms.Api.Delivery.Controllers.Content;
using Umbraco.Cms.Api.Delivery.Filters;
namespace Umbraco.Cms.Api.Delivery.Configuration;
/// <summary>
/// This configures member authentication for the Delivery API in Swagger. Consult the docs for
/// member authentication within the Delivery API for instructions on how to use this.
/// </summary>
/// <remarks>
/// This class is not used by the core CMS due to the required installation dependencies (local login page among other things).
/// </remarks>
public class ConfigureUmbracoMemberAuthenticationDeliveryApiSwaggerGenOptions : IConfigureOptions<SwaggerGenOptions>
{
private const string AuthSchemeName = "UmbracoMember";
public void Configure(SwaggerGenOptions options)
{
// add security requirements for content API operations
options.DocumentFilter<DeliveryApiSecurityFilter>();
options.OperationFilter<DeliveryApiSecurityFilter>();
}
private sealed class DeliveryApiSecurityFilter : SwaggerFilterBase<ContentApiControllerBase>, IOperationFilter, IDocumentFilter
{
public void Apply(OpenApiOperation operation, OperationFilterContext context)
{
if (CanApply(context) is false)
{
return;
}
var schemaRef = new OpenApiSecuritySchemeReference(AuthSchemeName, context.Document);
operation.Security ??= new List<OpenApiSecurityRequirement>();
operation.Security.Add(new OpenApiSecurityRequirement { [schemaRef] = [] });
}
public void Apply(OpenApiDocument swaggerDoc, DocumentFilterContext context)
{
if (context.DocumentName != DeliveryApiConfiguration.ApiName)
{
return;
}
swaggerDoc.AddComponent(
AuthSchemeName,
new OpenApiSecurityScheme
{
In = ParameterLocation.Header,
Name = AuthSchemeName,
Type = SecuritySchemeType.OAuth2,
Description = "Umbraco Member Authentication",
Flows = new OpenApiOAuthFlows
{
AuthorizationCode = new OpenApiOAuthFlow
{
AuthorizationUrl = new Uri(Paths.MemberApi.AuthorizationEndpoint, UriKind.Relative),
TokenUrl = new Uri(Paths.MemberApi.TokenEndpoint, UriKind.Relative),
},
},
});
}
}
}
@@ -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,
@@ -20,7 +20,7 @@ public abstract class DeliveryApiControllerBase : Controller, IUmbracoFeature
{
protected string DecodePath(string path)
{
// OpenAPI does not allow reserved chars as "in:path" parameters, so clients based on the Swagger JSON will URL
// OpenAPI does not allow reserved chars as "in:path" parameters, so clients based on the OpenAPI specification will URL
// encode the path. Normally, ASP.NET Core handles that encoding with an automatic decoding - apparently just not
// for forward slashes, for whatever reason... so we need to deal with those. Hopefully this will be addressed in
// an upcoming version of ASP.NET Core.
@@ -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.Options;
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;
@@ -51,7 +53,7 @@ public static class UmbracoBuilderExtensions
provider =>
{
HttpContext? httpContext = provider.GetRequiredService<IHttpContextAccessor>().HttpContext;
ApiVersion? apiVersion = httpContext?.GetRequestedApiVersion();
ApiVersion? apiVersion = httpContext?.RequestedApiVersion;
if (apiVersion is null)
{
return provider.GetRequiredService<RequestContextOutputExpansionStrategyV2>();
@@ -65,7 +67,6 @@ public static class UmbracoBuilderExtensions
ServiceLifetime.Scoped);
builder.Services.AddSingleton<IRequestCultureService, RequestCultureService>();
builder.Services.AddSingleton<IRequestSegmmentService, RequestSegmentService>();
builder.Services.AddSingleton<IRequestSegmentService, RequestSegmentService>();
builder.Services.AddSingleton<IRequestRoutingService, RequestRoutingService>();
builder.Services.AddSingleton<IRequestRedirectService, RequestRedirectService>();
@@ -84,19 +85,27 @@ public static class UmbracoBuilderExtensions
builder.Services.AddTransient<IRequestMemberAccessService, RequestMemberAccessService>();
builder.Services.AddTransient<ICurrentMemberClaimsProvider, CurrentMemberClaimsProvider>();
builder.Services.ConfigureOptions<ConfigureUmbracoDeliveryApiSwaggerGenOptions>();
builder.AddUmbracoApiOpenApiUI();
builder.AddUmbracoOpenApi();
builder.AddUmbracoOpenApiDocument<ConfigureUmbracoDeliveryApiOpenApiOptions>(
DeliveryApiConfiguration.ApiName,
DeliveryApiConfiguration.ApiTitle,
Constants.JsonOptionsNames.DeliveryApi);
builder
.Services
.AddControllers()
.AddJsonOptions(Constants.JsonOptionsNames.DeliveryApi, options =>
{
// all Delivery API specific JSON options go here
options.JsonSerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.CamelCase;
options.JsonSerializerOptions.TypeInfoResolver = new DeliveryApiJsonTypeResolver();
options.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter());
});
.AddJsonOptions(
Constants.JsonOptionsNames.DeliveryApi,
options =>
{
// all Delivery API specific JSON options go here
options.JsonSerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.CamelCase;
options.JsonSerializerOptions.TypeInfoResolver = new DeliveryApiJsonTypeResolver();
options.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter());
});
// Configures the JSON options for the Open API schema generation (based on the Delivery API MVC JSON options)
builder.Services.ConfigureOptions<ConfigureUmbracoDeliveryHttpJsonOptions>();
builder.Services.AddAuthentication();
builder.AddUmbracoOpenIddict();
@@ -105,6 +114,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 +145,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 +154,24 @@ 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>();
builder.AddNotificationAsyncHandler<ElementCacheRefresherNotification, DeliveryApiElementOutputCacheEvictionHandler>();
// Register extension point default implementations.
builder.Services.AddSingleton<IDeliveryApiOutputCacheTagProvider, DeliveryApiContentTypeOutputCacheTagProvider>();
builder.Services.AddUnique<IDeliveryApiOutputCacheRequestFilter, DefaultDeliveryApiOutputCacheRequestFilter>();
builder.Services.AddUnique<IDeliveryApiOutputCacheManager, DeliveryApiOutputCacheManager>();
return builder;
}
}
@@ -1,127 +0,0 @@
using System.Text.Json.Nodes;
using Microsoft.OpenApi;
using Swashbuckle.AspNetCore.SwaggerGen;
using Umbraco.Cms.Api.Delivery.Configuration;
using Umbraco.Cms.Api.Delivery.Controllers.Content;
using Umbraco.Cms.Core;
namespace Umbraco.Cms.Api.Delivery.Filters;
internal sealed class SwaggerContentDocumentationFilter : SwaggerDocumentationFilterBase<ContentApiControllerBase>
{
protected override string DocumentationLink => DeliveryApiConfiguration.ApiDocumentationContentArticleLink;
protected override void ApplyOperation(OpenApiOperation operation, OperationFilterContext context)
{
operation.Parameters ??= new List<IOpenApiParameter>();
AddExpand(operation, context);
AddFields(operation, context);
operation.Parameters.Add(new OpenApiParameter
{
Name = Constants.DeliveryApi.HeaderNames.AcceptLanguage,
In = ParameterLocation.Header,
Required = false,
Description = "Defines the language to return. Use this when querying language variant content items.",
Schema = new OpenApiSchema { Type = JsonSchemaType.String },
Examples = new Dictionary<string, IOpenApiExample>
{
{ "Default", new OpenApiExample { Value = string.Empty } },
{ "English culture", new OpenApiExample { Value = "en-us" } },
},
});
operation.Parameters.Add(new OpenApiParameter
{
Name = Constants.DeliveryApi.HeaderNames.AcceptSegment,
In = ParameterLocation.Header,
Required = false,
Description = "Defines the segment to return. Use this when querying segment variant content items.",
Schema = new OpenApiSchema { Type = JsonSchemaType.String },
Examples = new Dictionary<string, IOpenApiExample>
{
{ "Default", new OpenApiExample { Value = string.Empty } },
{ "Segment One", new OpenApiExample { Value = "segment-one" } },
},
});
AddApiKey(operation);
operation.Parameters.Add(new OpenApiParameter
{
Name = Constants.DeliveryApi.HeaderNames.Preview,
In = ParameterLocation.Header,
Required = false,
Description = "Whether to request draft content.",
Schema = new OpenApiSchema { Type = JsonSchemaType.Boolean },
});
operation.Parameters.Add(new OpenApiParameter
{
Name = Constants.DeliveryApi.HeaderNames.StartItem,
In = ParameterLocation.Header,
Required = false,
Description = "URL segment or GUID of a root content item.",
Schema = new OpenApiSchema { Type = JsonSchemaType.String },
});
}
protected override void ApplyParameter(OpenApiParameter parameter, ParameterFilterContext context)
{
switch (parameter.Name)
{
case "fetch":
AddQueryParameterDocumentation(parameter, FetchQueryParameterExamples(), "Specifies the content items to fetch");
break;
case "filter":
AddQueryParameterDocumentation(parameter, FilterQueryParameterExamples(), "Defines how to filter the fetched content items");
break;
case "sort":
AddQueryParameterDocumentation(parameter, SortQueryParameterExamples(), "Defines how to sort the found content items");
break;
case "skip":
parameter.Description = PaginationDescription(true, "content");
break;
case "take":
parameter.Description = PaginationDescription(false, "content");
break;
default:
return;
}
}
private Dictionary<string, IOpenApiExample> FetchQueryParameterExamples() =>
new()
{
{ "Select all", new OpenApiExample { Value = string.Empty } },
{ "Select all ancestors of a node by id", new OpenApiExample { Value = "ancestors:id" } },
{ "Select all ancestors of a node by path", new OpenApiExample { Value = "ancestors:path" } },
{ "Select all children of a node by id", new OpenApiExample { Value = "children:id" } },
{ "Select all children of a node by path", new OpenApiExample { Value = "children:path" } },
{ "Select all descendants of a node by id", new OpenApiExample { Value = "descendants:id" } },
{ "Select all descendants of a node by path", new OpenApiExample { Value = "descendants:path" } },
};
private Dictionary<string, IOpenApiExample> FilterQueryParameterExamples() =>
new()
{
{ "Default filter", new OpenApiExample { Value = string.Empty } },
{ "Filter by content type (equals)", new OpenApiExample { Value = new JsonArray { "contentType:alias1" } } },
{ "Filter by name (contains)", new OpenApiExample { Value = new JsonArray { "name:nodeName" } } },
{ "Filter by creation date (less than)", new OpenApiExample { Value = new JsonArray { "createDate<2024-01-01" } } },
{ "Filter by update date (greater than or equal)", new OpenApiExample { Value = new JsonArray { "updateDate>:2023-01-01" } } },
};
private Dictionary<string, IOpenApiExample> SortQueryParameterExamples() =>
new()
{
{ "Default sort", new OpenApiExample { Value = string.Empty } },
{ "Sort by create date", new OpenApiExample { Value = new JsonArray { "createDate:asc", "createDate:desc" } } },
{ "Sort by level", new OpenApiExample { Value = new JsonArray { "level:asc", "level:desc" } } },
{ "Sort by name", new OpenApiExample { Value = new JsonArray { "name:asc", "name:desc" } } },
{ "Sort by sort order", new OpenApiExample { Value = new JsonArray { "sortOrder:asc", "sortOrder:desc" } } },
{ "Sort by update date", new OpenApiExample { Value = new JsonArray { "updateDate:asc", "updateDate:desc" } } },
};
}
@@ -1,155 +0,0 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.OpenApi;
using Swashbuckle.AspNetCore.SwaggerGen;
using Umbraco.Cms.Core;
namespace Umbraco.Cms.Api.Delivery.Filters;
internal abstract class SwaggerDocumentationFilterBase<TBaseController>
: SwaggerFilterBase<TBaseController>, IOperationFilter, IParameterFilter
where TBaseController : Controller
{
protected abstract string DocumentationLink { get; }
public void Apply(OpenApiOperation operation, OperationFilterContext context)
{
if (CanApply(context))
{
ApplyOperation(operation, context);
}
}
public void Apply(IOpenApiParameter parameter, ParameterFilterContext context)
{
if (CanApply(context) && parameter is OpenApiParameter openApiParameter)
{
ApplyParameter(openApiParameter, context);
}
}
protected abstract void ApplyOperation(OpenApiOperation operation, OperationFilterContext context);
protected abstract void ApplyParameter(OpenApiParameter parameter, ParameterFilterContext context);
protected void AddQueryParameterDocumentation(OpenApiParameter parameter, Dictionary<string, IOpenApiExample> examples, string description)
{
parameter.Description = QueryParameterDescription(description);
parameter.Examples = examples;
}
protected void AddExpand(OpenApiOperation operation, OperationFilterContext context)
{
if (IsApiV1(context))
{
AddExpandV1(operation);
}
else
{
AddExpand(operation);
}
}
protected void AddFields(OpenApiOperation operation, OperationFilterContext context)
{
if (IsApiV1(context))
{
// "fields" is not a thing in Delivery API V1
return;
}
AddFields(operation);
}
protected void AddApiKey(OpenApiOperation operation)
{
operation.Parameters ??= new List<IOpenApiParameter>();
operation.Parameters.Add(
new OpenApiParameter
{
Name = Constants.DeliveryApi.HeaderNames.ApiKey,
In = ParameterLocation.Header,
Required = false,
Description = "API key specified through configuration to authorize access to the API.",
Schema = new OpenApiSchema { Type = JsonSchemaType.String },
});
}
protected string PaginationDescription(bool skip, string itemType)
=> $"Specifies the number of found {itemType} items to {(skip ? "skip" : "take")}. Use this to control pagination of the response.";
private string QueryParameterDescription(string description)
=> $"{description}. Refer to [the documentation]({DocumentationLink}#query-parameters) for more details on this.";
// FIXME: remove this when Delivery API V1 has been removed (expectedly in V15)
private static bool IsApiV1(OperationFilterContext context)
=> context.ApiDescription.RelativePath?.Contains("api/v1") is true;
// FIXME: remove this when Delivery API V1 has been removed (expectedly in V15)
private void AddExpandV1(OpenApiOperation operation)
{
operation.Parameters ??= new List<IOpenApiParameter>();
operation.Parameters.Add(
new OpenApiParameter
{
Name = "expand",
In = ParameterLocation.Query,
Required = false,
Description =
QueryParameterDescription("Defines the properties that should be expanded in the response"),
Schema = new OpenApiSchema { Type = JsonSchemaType.String },
Examples = new Dictionary<string, IOpenApiExample>
{
{ "Expand none", new OpenApiExample { Value = string.Empty } },
{ "Expand all", new OpenApiExample { Value = "all" } },
{ "Expand specific property", new OpenApiExample { Value = "property:alias1" } },
{ "Expand specific properties", new OpenApiExample { Value = "property:alias1,alias2" } },
},
});
}
private void AddExpand(OpenApiOperation operation)
{
operation.Parameters ??= new List<IOpenApiParameter>();
operation.Parameters.Add(
new OpenApiParameter
{
Name = "expand",
In = ParameterLocation.Query,
Required = false,
Description =
QueryParameterDescription("Defines the properties that should be expanded in the response"),
Schema = new OpenApiSchema { Type = JsonSchemaType.String },
Examples = new Dictionary<string, IOpenApiExample>
{
{ "Expand none", new OpenApiExample { Value = string.Empty } },
{ "Expand all properties", new OpenApiExample { Value = "properties[$all]" } },
{ "Expand specific property", new OpenApiExample { Value = "properties[alias1]" } },
{ "Expand specific properties", new OpenApiExample { Value = "properties[alias1,alias2]" } },
{ "Expand nested properties", new OpenApiExample { Value = "properties[alias1[properties[nestedAlias1,nestedAlias2]]]" } },
},
});
}
private void AddFields(OpenApiOperation operation)
{
operation.Parameters ??= new List<IOpenApiParameter>();
operation.Parameters.Add(
new OpenApiParameter
{
Name = "fields",
In = ParameterLocation.Query,
Required = false,
Description =
QueryParameterDescription(
"Explicitly defines which properties should be included in the response (by default all properties are included)"),
Schema = new OpenApiSchema { Type = JsonSchemaType.String },
Examples = new Dictionary<string, IOpenApiExample>
{
{ "Include all properties", new OpenApiExample { Value = "properties[$all]" } },
{ "Include only specific property", new OpenApiExample { Value = "properties[alias1]" } },
{ "Include only specific properties", new OpenApiExample { Value = "properties[alias1,alias2]" } },
{ "Include only specific nested properties", new OpenApiExample { Value = "properties[alias1[properties[nestedAlias1,nestedAlias2]]]" } },
},
});
}
}
@@ -1,19 +0,0 @@
using System.Reflection;
using Microsoft.AspNetCore.Mvc;
using Swashbuckle.AspNetCore.SwaggerGen;
using Umbraco.Extensions;
namespace Umbraco.Cms.Api.Delivery.Filters;
internal abstract class SwaggerFilterBase<TBaseController>
where TBaseController : Controller
{
protected bool CanApply(OperationFilterContext context)
=> CanApply(context.MethodInfo);
protected bool CanApply(ParameterFilterContext context)
=> CanApply(context.ParameterInfo.Member);
private bool CanApply(MemberInfo member)
=> member.DeclaringType?.Implements<TBaseController>() is true;
}
@@ -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);
}
}
}
@@ -51,7 +51,7 @@ public abstract class DeliveryApiVersionAwareJsonConverterBase<T> : JsonConverte
private int? GetApiVersion()
{
HttpContext? httpContext = _httpContextAccessor.HttpContext;
ApiVersion? apiVersion = httpContext?.GetRequestedApiVersion();
ApiVersion? apiVersion = httpContext?.RequestedApiVersion;
return apiVersion?.MajorVersion;
}
@@ -0,0 +1,19 @@
using Microsoft.AspNetCore.OpenApi;
using Microsoft.OpenApi;
namespace Umbraco.Cms.Api.Delivery.OpenApi.Extensions;
/// <summary>
/// Provides extension methods for <see cref="OpenApiSchemaTransformerContext"/>.
/// </summary>
internal static class OpenApiSchemaTransformerContextExtensions
{
/// <summary>
/// Gets the OpenAPI document from the context, throwing if it is null.
/// </summary>
/// <param name="context">The schema transformer context.</param>
/// <returns>The OpenAPI document.</returns>
/// <exception cref="InvalidOperationException">Thrown when the document is null.</exception>
public static OpenApiDocument GetRequiredDocument(this OpenApiSchemaTransformerContext context)
=> context.Document ?? throw new InvalidOperationException("OpenAPI document context is required for schema registration.");
}
@@ -0,0 +1,34 @@
using Microsoft.AspNetCore.OpenApi;
using Microsoft.Extensions.DependencyInjection;
using Umbraco.Cms.Api.Delivery.Configuration;
using Umbraco.Cms.Api.Delivery.OpenApi.Transformers;
namespace Umbraco.Cms.Api.Delivery.OpenApi;
/// <summary>
/// Extension methods for configuring OpenAPI support for the Delivery API.
/// </summary>
public static class OpenApiServiceCollectionExtensions
{
/// <summary>
/// Adds member authentication support to the Delivery API OpenAPI document.
/// </summary>
/// <param name="services">The <see cref="IServiceCollection"/> to configure.</param>
/// <returns>The configured <see cref="IServiceCollection"/> instance.</returns>
/// <remarks>
/// This enables the OAuth2 authorization code flow for member authentication in Swagger UI.
/// Consult the Delivery API member authentication documentation for setup instructions.
/// </remarks>
public static IServiceCollection AddDeliveryApiOpenApiMemberAuthentication(this IServiceCollection services)
{
services.PostConfigure<OpenApiOptions>(
DeliveryApiConfiguration.ApiName,
options =>
{
options.AddDocumentTransformer<MemberAuthenticationSecurityRequirementsTransformer>();
options.AddOperationTransformer<MemberAuthenticationSecurityRequirementsTransformer>();
});
return services;
}
}
@@ -0,0 +1,51 @@
using Microsoft.AspNetCore.OpenApi;
using Microsoft.OpenApi;
using Umbraco.Cms.Core;
namespace Umbraco.Cms.Api.Delivery.OpenApi.Transformers;
/// <summary>
/// Transforms the OpenAPI document to include API key security scheme.
/// </summary>
internal class ApiKeyTransformer : IOpenApiDocumentTransformer, IOpenApiOperationTransformer
{
private const string AuthSchemeName = "ApiKeyAuth";
/// <inheritdoc/>
public Task TransformAsync(
OpenApiDocument document,
OpenApiDocumentTransformerContext context,
CancellationToken cancellationToken)
{
var apiKeyScheme = new OpenApiSecurityScheme
{
Type = SecuritySchemeType.ApiKey,
Name = Constants.DeliveryApi.HeaderNames.ApiKey,
In = ParameterLocation.Header,
Description = "API key specified through configuration to authorize access to the API.",
};
document.Components ??= new OpenApiComponents();
document.Components.SecuritySchemes ??= new Dictionary<string, IOpenApiSecurityScheme>();
document.Components.SecuritySchemes[AuthSchemeName] = apiKeyScheme;
var schemaRef = new OpenApiSecuritySchemeReference(AuthSchemeName, document);
document.Security ??= new List<OpenApiSecurityRequirement>();
document.Security.Add(new OpenApiSecurityRequirement { [schemaRef] = [] });
return Task.CompletedTask;
}
/// <inheritdoc/>
public Task TransformAsync(
OpenApiOperation operation,
OpenApiOperationTransformerContext context,
CancellationToken cancellationToken)
{
var schemaRef = new OpenApiSecuritySchemeReference(AuthSchemeName, context.Document);
operation.Security ??= new List<OpenApiSecurityRequirement>();
operation.Security.Add(new OpenApiSecurityRequirement { [schemaRef] = [] });
return Task.CompletedTask;
}
}
@@ -0,0 +1,146 @@
using System.Text.Json.Nodes;
using Microsoft.AspNetCore.Mvc.Controllers;
using Microsoft.AspNetCore.OpenApi;
using Microsoft.OpenApi;
using Umbraco.Cms.Api.Delivery.Configuration;
using Umbraco.Cms.Api.Delivery.Controllers.Content;
using Umbraco.Cms.Core;
using Umbraco.Extensions;
namespace Umbraco.Cms.Api.Delivery.OpenApi.Transformers;
/// <summary>
/// Transforms OpenAPI operations for the Content API, adding relevant parameters and documentation.
/// </summary>
internal sealed class ContentApiTransformer : DeliveryApiTransformerBase
{
/// <inheritdoc/>
protected override string DocumentationLink => DeliveryApiConfiguration.ApiDocumentationContentArticleLink;
/// <inheritdoc/>
protected override bool ShouldApply(OpenApiOperationTransformerContext context) =>
context.Description.ActionDescriptor is ControllerActionDescriptor description
&& description.ControllerTypeInfo.Implements<ContentApiControllerBase>();
/// <inheritdoc/>
protected override Task ApplyAsync(
OpenApiOperation operation,
OpenApiOperationTransformerContext context,
CancellationToken cancellationToken)
{
operation.Parameters ??= new List<IOpenApiParameter>();
operation.Parameters.Add(
new OpenApiParameter
{
Name = Constants.DeliveryApi.HeaderNames.AcceptLanguage,
In = ParameterLocation.Header,
Required = false,
Description = "Defines the language to return. Use this when querying language variant content items.",
Schema = new OpenApiSchema { Type = JsonSchemaType.String },
Examples = new Dictionary<string, IOpenApiExample>
{
{ "Default", new OpenApiExample { Value = string.Empty } },
{ "English culture", new OpenApiExample { Value = "en-us" } },
},
});
operation.Parameters.Add(
new OpenApiParameter
{
Name = Constants.DeliveryApi.HeaderNames.AcceptSegment,
In = ParameterLocation.Header,
Required = false,
Description = "Defines the segment to return. Use this when querying segment variant content items.",
Schema = new OpenApiSchema { Type = JsonSchemaType.String },
Examples = new Dictionary<string, IOpenApiExample>
{
{ "Default", new OpenApiExample { Value = string.Empty } },
{ "Segment One", new OpenApiExample { Value = "segment-one" } },
},
});
operation.Parameters.Add(
new OpenApiParameter
{
Name = Constants.DeliveryApi.HeaderNames.Preview,
In = ParameterLocation.Header,
Required = false,
Description = "Whether to request draft content.",
Schema = new OpenApiSchema { Type = JsonSchemaType.Boolean },
});
operation.Parameters.Add(
new OpenApiParameter
{
Name = Constants.DeliveryApi.HeaderNames.StartItem,
In = ParameterLocation.Header,
Required = false,
Description = "URL segment or GUID of a root content item.",
Schema = new OpenApiSchema { Type = JsonSchemaType.String },
});
foreach (OpenApiParameter parameter in operation.Parameters?.OfType<OpenApiParameter>() ?? [])
{
ApplyParameter(parameter);
}
return Task.CompletedTask;
}
private void ApplyParameter(OpenApiParameter parameter)
{
switch (parameter.Name)
{
case "fetch":
AddQueryParameterDocumentation(parameter, FetchQueryParameterExamples(), "Specifies the content items to fetch");
break;
case "filter":
AddQueryParameterDocumentation(parameter, FilterQueryParameterExamples(), "Defines how to filter the fetched content items");
break;
case "sort":
AddQueryParameterDocumentation(parameter, SortQueryParameterExamples(), "Defines how to sort the found content items");
break;
case "skip":
parameter.Description = PaginationDescription(true, "content");
break;
case "take":
parameter.Description = PaginationDescription(false, "content");
break;
default:
return;
}
}
private Dictionary<string, IOpenApiExample> FetchQueryParameterExamples() =>
new()
{
{ "Select all", new OpenApiExample { Value = "" } },
{ "Select all ancestors of a node by id", new OpenApiExample { Value = "ancestors:id" } },
{ "Select all ancestors of a node by path", new OpenApiExample { Value = "ancestors:path" } },
{ "Select all children of a node by id", new OpenApiExample { Value = "children:id" } },
{ "Select all children of a node by path", new OpenApiExample { Value = "children:path" } },
{ "Select all descendants of a node by id", new OpenApiExample { Value = "descendants:id" } },
{ "Select all descendants of a node by path", new OpenApiExample { Value = "descendants:path" } },
};
private Dictionary<string, IOpenApiExample> FilterQueryParameterExamples() =>
new()
{
{ "Default filter", new OpenApiExample { Value = new JsonArray("") } },
{ "Filter by content type (equals)", new OpenApiExample { Value = new JsonArray("contentType:alias1") } },
{ "Filter by name (contains)", new OpenApiExample { Value = new JsonArray("name:nodeName") } },
{ "Filter by creation date (less than)", new OpenApiExample { Value = new JsonArray("createDate<2024-01-01") } },
{ "Filter by update date (greater than or equal)", new OpenApiExample { Value = new JsonArray("updateDate>:2023-01-01") } },
};
private Dictionary<string, IOpenApiExample> SortQueryParameterExamples() =>
new()
{
{ "Default sort", new OpenApiExample { Value = new JsonArray("") } },
{ "Sort by create date", new OpenApiExample { Value = new JsonArray("createDate:asc", "createDate:desc") } },
{ "Sort by level", new OpenApiExample { Value = new JsonArray("level:asc", "level:desc") } },
{ "Sort by name", new OpenApiExample { Value = new JsonArray("name:asc", "name:desc") } },
{ "Sort by sort order", new OpenApiExample { Value = new JsonArray("sortOrder:asc", "sortOrder:desc") } },
{ "Sort by update date", new OpenApiExample { Value = new JsonArray("updateDate:asc", "updateDate:desc") } },
};
}
@@ -0,0 +1,691 @@
using System.Diagnostics.CodeAnalysis;
using System.Text.Json;
using System.Text.Json.Serialization.Metadata;
using Microsoft.AspNetCore.Http.Json;
using Microsoft.AspNetCore.OpenApi;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Microsoft.OpenApi;
using Umbraco.Cms.Api.Common.Configuration;
using Umbraco.Cms.Api.Delivery.OpenApi.Extensions;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Configuration.Models;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.DeliveryApi;
using Umbraco.Cms.Core.Models.PublishedContent;
using Umbraco.Cms.Core.Services;
using Umbraco.Extensions;
namespace Umbraco.Cms.Api.Delivery.OpenApi.Transformers;
/// <summary>
/// Transforms the OpenAPI document to add schemas for the instance's document types.
/// </summary>
/// <remarks>
/// <para>
/// This transformer implements both <see cref="IOpenApiSchemaTransformer"/> and <see cref="IOpenApiDocumentTransformer"/>
/// to handle schema generation in two phases:
/// </para>
/// <para>
/// <b>Phase 1 - Schema Transformation:</b> When the schema transformer encounters types like
/// <see cref="IApiContentResponse"/> or <see cref="IApiMediaWithCrops"/>, it generates content-type-specific
/// schemas (e.g., "ArticleContentResponseModel") and registers them as components in the OpenAPI document.
/// </para>
/// <para>
/// <b>Circular Reference Handling:</b> Content type schemas can reference each other (e.g., a "Page"
/// might have a property of type "Article", which might reference "Page" again). To prevent infinite recursion
/// during schema generation, we use a placeholder pattern:
/// <list type="bullet">
/// <item>When generating a schema, we track its ID in <c>_handledSchemas</c></item>
/// <item>If we encounter the same schema ID again (circular reference), we return a temporary placeholder
/// schema with metadata marking it for later replacement</item>
/// <item>The placeholder contains a <c>x-recursive-ref</c> metadata key with the target schema ID</item>
/// </list>
/// </para>
/// <para>
/// <b>Phase 2 - Document Transformation:</b> After all schemas are generated, the document transformer
/// resolves inline schemas into proper <c>$ref</c> references. This handles two cases:
/// <list type="bullet">
/// <item>Circular reference placeholders (marked with <c>x-recursive-ref</c>) created during Phase 1</item>
/// <item>Componentized schemas (marked with <c>x-schema-id</c>) that the framework did not automatically
/// resolve to <c>$ref</c> — this can happen for schemas reached through properties or composition
/// rather than as direct API response types</item>
/// </list>
/// This is done by <see cref="ResolveSchemaReferences(OpenApiDocument, IOpenApiSchema)"/> which recursively walks
/// through all schemas and substitutes matching entries with <see cref="OpenApiSchemaReference"/> instances.
/// </para>
/// </remarks>
public sealed class ContentTypeSchemaTransformer : IOpenApiSchemaTransformer, IOpenApiDocumentTransformer
{
// Metadata keys
private const string RecursiveRefMetadataKey = "x-recursive-ref";
private const string SchemaIdMetadataKey = "x-schema-id";
// Schema ID suffixes
private const string ResponseModelSuffix = "ResponseModel";
private const string ModelSuffix = "Model";
private const string ContentSuffix = "Content";
private const string ElementSuffix = "Element";
private const string MediaSuffix = "Media";
private const string MediaWithCropsSuffix = "MediaWithCrops";
private const string PropertiesModelSuffix = "PropertiesModel";
private readonly IContentTypeSchemaService _contentTypeSchemaService;
private readonly IOptionsMonitor<DeliveryApiSettings> _deliveryApiSettings;
private readonly ILogger<ContentTypeSchemaTransformer> _logger;
private readonly IJsonTypeInfoResolver _jsonTypeInfoResolver;
/// <summary>
/// Tracks schema IDs that have been or are being generated to detect circular references.
/// When a schema ID is encountered a second time, a placeholder is returned instead of recursing infinitely.
/// </summary>
private readonly HashSet<string> _handledSchemas = [];
private readonly JsonSerializerOptions _serializerOptions;
/// <summary>
/// Initializes a new instance of the <see cref="ContentTypeSchemaTransformer"/> class.
/// </summary>
/// <param name="contentTypeSchemaService">The content type info service.</param>
/// <param name="jsonOptionsMonitor">The JSON options monitor.</param>
/// <param name="deliveryApiSettings">The Delivery API settings, used to honour the allow/deny content type list.</param>
/// <param name="logger">The logger.</param>
public ContentTypeSchemaTransformer(
IContentTypeSchemaService contentTypeSchemaService,
IOptionsMonitor<JsonOptions> jsonOptionsMonitor,
IOptionsMonitor<DeliveryApiSettings> deliveryApiSettings,
ILogger<ContentTypeSchemaTransformer> logger)
{
_contentTypeSchemaService = contentTypeSchemaService;
_deliveryApiSettings = deliveryApiSettings;
_logger = logger;
_serializerOptions = jsonOptionsMonitor
.Get(Constants.JsonOptionsNames.DeliveryApi)
.SerializerOptions;
_jsonTypeInfoResolver = _serializerOptions.TypeInfoResolver
?? throw new InvalidOperationException("The JSON serializer options must have a TypeInfoResolver configured.");
}
private IReadOnlyCollection<ContentTypeSchemaInfo> DocumentTypes
=> field ??= FilterAllowedDocumentTypes(_contentTypeSchemaService.GetDocumentTypes());
private IReadOnlyCollection<ContentTypeSchemaInfo> MediaTypes
=> field ??= _contentTypeSchemaService.GetMediaTypes();
/// <inheritdoc />
public Task TransformAsync(
OpenApiDocument document,
OpenApiDocumentTransformerContext context,
CancellationToken cancellationToken)
{
if (document.Components?.Schemas is not { Count: > 0 })
{
return Task.CompletedTask;
}
foreach (IOpenApiSchema componentsSchema in document.Components.Schemas.Values)
{
ResolveSchemaReferences(document, componentsSchema);
}
return Task.CompletedTask;
}
/// <inheritdoc />
public async Task TransformAsync(
OpenApiSchema schema,
OpenApiSchemaTransformerContext context,
CancellationToken cancellationToken)
{
switch (context.JsonTypeInfo.Type)
{
case var type when type == typeof(IApiContentResponse):
await ApplyPolymorphicContentType(
schema,
context,
PublishedItemType.Content,
DocumentTypes.Where(c => !c.IsElement),
async (contentType, derivedTypeSchemas) =>
{
var schemaIdPrefix = $"{contentType.SchemaId}{ContentSuffix}";
return await CreateContentTypeResponseSchema(
schemaIdPrefix,
derivedTypeSchemas,
context);
},
cancellationToken);
await CreateSchema(GetJsonTypeInfo(typeof(IApiContent)), context, cancellationToken);
return;
case var type when type == typeof(IApiContent):
await ApplyPolymorphicContentType(
schema,
context,
PublishedItemType.Content,
DocumentTypes.Where(c => !c.IsElement),
async (contentType, derivedTypeSchemas) =>
{
var schemaId = $"{contentType.SchemaId}{ContentSuffix}{ModelSuffix}";
return await CreateContentTypeSchema(
schemaId,
PublishedItemType.Content,
contentType,
derivedTypeSchemas,
context,
cancellationToken);
},
cancellationToken);
await CreateSchema(GetJsonTypeInfo(typeof(IApiElement)), context, cancellationToken);
return;
case var type when type == typeof(IApiElement):
await ApplyPolymorphicContentType(
schema,
context,
PublishedItemType.Content,
DocumentTypes.Where(c => c.IsElement),
async (contentType, derivedTypeSchemas) =>
{
var schemaId = $"{contentType.SchemaId}{ElementSuffix}{ModelSuffix}";
return await CreateContentTypeSchema(
schemaId,
PublishedItemType.Content,
contentType,
derivedTypeSchemas,
context,
cancellationToken);
},
cancellationToken);
return;
case var type when type == typeof(IApiMediaWithCropsResponse):
await ApplyPolymorphicContentType(
schema,
context,
PublishedItemType.Media,
MediaTypes,
async (contentType, derivedTypeSchemas) =>
{
var schemaId = $"{contentType.SchemaId}{MediaWithCropsSuffix}";
return await CreateContentTypeResponseSchema(
schemaId,
derivedTypeSchemas,
context);
},
cancellationToken);
await CreateSchema(GetJsonTypeInfo(typeof(IApiMediaWithCrops)), context, cancellationToken);
return;
case var type when type == typeof(IApiMediaWithCrops):
await ApplyPolymorphicContentType(
schema,
context,
PublishedItemType.Media,
MediaTypes,
async (contentType, derivedTypeSchemas) =>
{
var schemaId = $"{contentType.SchemaId}{MediaWithCropsSuffix}{ModelSuffix}";
return await CreateContentTypeSchema(
schemaId,
PublishedItemType.Media,
contentType,
derivedTypeSchemas,
context,
cancellationToken);
},
cancellationToken);
return;
default:
// HACK: Some types with circular references (e.g. ApiBlockGridItem) get left
// inlined by the framework, breaking $ref resolution. Register them explicitly.
if (GetSchemaId(context.JsonTypeInfo) is not { } schemaId || !_handledSchemas.Add(schemaId))
{
return;
}
OpenApiDocument document = context.GetRequiredDocument();
document.AddComponent(schemaId, schema);
return;
}
}
private async Task ApplyPolymorphicContentType(
OpenApiSchema schema,
OpenApiSchemaTransformerContext context,
PublishedItemType itemType,
IEnumerable<ContentTypeSchemaInfo> contentTypes,
Func<ContentTypeSchemaInfo, List<IOpenApiSchema>, Task<OpenApiSchema>> contentTypeSchemaFactory,
CancellationToken cancellationToken)
{
List<IOpenApiSchema> derivedTypeSchemas = await ResolveDerivedTypeSchemas(
schema,
context,
cancellationToken);
OpenApiDocument document = context.GetRequiredDocument();
var typePropertyName = GetTypePropertyName(itemType);
schema.Discriminator = new OpenApiDiscriminator
{
PropertyName = typePropertyName,
Mapping = new Dictionary<string, OpenApiSchemaReference>(),
};
schema.OneOf ??= new List<IOpenApiSchema>();
foreach (ContentTypeSchemaInfo contentType in contentTypes)
{
OpenApiSchema contentTypeSchema = await contentTypeSchemaFactory(contentType, derivedTypeSchemas);
var schemaId = (string)contentTypeSchema.Metadata![SchemaIdMetadataKey];
schema.Discriminator.Mapping[contentType.Alias] = new OpenApiSchemaReference(schemaId, document);
schema.OneOf.Add(contentTypeSchema);
}
// Remove all schema properties that are now handled by the derived types
schema.AnyOf = null;
schema.Properties = null;
schema.Required = new HashSet<string> { typePropertyName };
}
/// <summary>
/// Creates and adds a schema to the OpenAPI document if it does not already exist.
/// </summary>
/// <remarks>A placeholder schema is added first to avoid recursion issues when generating schemas that reference themselves.</remarks>
private async Task<IOpenApiSchema> CreateSchema(
JsonTypeInfo jsonTypeInfo,
OpenApiSchemaTransformerContext context,
CancellationToken cancellationToken)
{
if (jsonTypeInfo.Type.IsArray || jsonTypeInfo.Kind == JsonTypeInfoKind.Enumerable)
{
Type elementType = jsonTypeInfo.ElementType ?? jsonTypeInfo.Type.GetElementType() ?? typeof(object);
JsonTypeInfo elementJsonTypeInfo = GetJsonTypeInfo(elementType);
IOpenApiSchema itemSchema = await CreateSchema(elementJsonTypeInfo, context, cancellationToken);
return new OpenApiSchema
{
Type = JsonSchemaType.Array,
Items = itemSchema,
};
}
var schemaId = GetSchemaId(jsonTypeInfo);
// If this is one of the types we handle, and we already started generating it, return a placeholder
// to avoid circular reference issues.
// In the document transformer, these placeholders will be replaced with the actual schemas.
if (schemaId is not null && !_handledSchemas.Add(schemaId))
{
return GetPlaceholderSchema(schemaId);
}
OpenApiSchema schema;
try
{
schema = await context.GetOrCreateSchemaAsync(
jsonTypeInfo.Type,
cancellationToken: cancellationToken);
}
catch (Exception ex)
{
// Log the error but continue with a fallback schema to avoid failing the entire document generation.
// The fallback schema includes a description indicating the failure, making it visible to API consumers.
_logger.LogError(ex, "Failed to create OpenAPI schema for type {TypeName}", jsonTypeInfo.Type.FullName);
schema = new OpenApiSchema
{
Description = $"[Schema generation failed for type '{jsonTypeInfo.Type.FullName}'. See server logs for details.]",
};
}
if (schemaId is null)
{
return schema;
}
OpenApiDocument document = context.GetRequiredDocument();
document.AddComponent(schemaId, schema);
return new OpenApiSchemaReference(schemaId, document);
}
/// <summary>
/// Allows null at a property reference site without mutating any shared component schema.
/// Inline schemas have <c>null</c> OR-ed into their <c>type</c> flags; schema references and
/// recursive-ref placeholders are wrapped in a <c>oneOf</c> with an explicit null branch so the
/// shared component is left unchanged.
/// </summary>
private static IOpenApiSchema AsNullable(IOpenApiSchema schema)
{
if (schema is OpenApiSchema inline
&& inline.Metadata?.ContainsKey(RecursiveRefMetadataKey) is not true)
{
inline.Type |= JsonSchemaType.Null;
return inline;
}
return new OpenApiSchema
{
OneOf =
[
schema,
new OpenApiSchema { Type = JsonSchemaType.Null },
],
};
}
private static Task<OpenApiSchema> CreateContentTypeResponseSchema(
string schemaIdPrefix,
List<IOpenApiSchema> derivedTypeSchemas,
OpenApiSchemaTransformerContext context)
{
var schemaId = $"{schemaIdPrefix}{ResponseModelSuffix}";
OpenApiDocument document = context.GetRequiredDocument();
var schema = new OpenApiSchema
{
Type = JsonSchemaType.Object,
AllOf = [..derivedTypeSchemas, new OpenApiSchemaReference($"{schemaIdPrefix}{ModelSuffix}", document)],
Metadata = new Dictionary<string, object> { [SchemaIdMetadataKey] = schemaId },
};
document.AddComponent(schemaId, schema);
return Task.FromResult(schema);
}
private async Task<OpenApiSchema> CreateContentTypeSchema(
string schemaId,
PublishedItemType itemType,
ContentTypeSchemaInfo contentType,
List<IOpenApiSchema> derivedTypeSchemas,
OpenApiSchemaTransformerContext context,
CancellationToken cancellationToken)
{
var typePropertyName = GetTypePropertyName(itemType);
var schema = new OpenApiSchema
{
Type = JsonSchemaType.Object,
Properties = new Dictionary<string, IOpenApiSchema>
{
[typePropertyName] = new OpenApiSchema { Const = contentType.Alias },
["properties"] = await CreatePropertiesSchema(contentType, itemType, context, cancellationToken),
},
Required = new HashSet<string> { typePropertyName },
AllOf = derivedTypeSchemas.Count > 0 ? derivedTypeSchemas : null,
Metadata = new Dictionary<string, object> { [SchemaIdMetadataKey] = schemaId, },
};
OpenApiDocument document = context.GetRequiredDocument();
document.AddComponent(schemaId, schema);
return schema;
}
private async Task<OpenApiSchemaReference> CreatePropertiesSchema(
ContentTypeSchemaInfo contentType,
PublishedItemType itemType,
OpenApiSchemaTransformerContext context,
CancellationToken cancellationToken)
{
var schemaId = GetPropertiesModelSchemaId(contentType, itemType);
var propertiesSchema = new OpenApiSchema
{
Type = JsonSchemaType.Object,
AllOf =
[
..contentType.CompositionSchemaIds.Select(compositionSchemaId
=> GetPlaceholderSchema(GetCompositionPropertiesModelSchemaId(compositionSchemaId, itemType)))
],
Properties = await CreateContentTypeProperties(contentType, context, cancellationToken),
Metadata = new Dictionary<string, object> { [SchemaIdMetadataKey] = schemaId },
};
OpenApiDocument document = context.GetRequiredDocument();
document.AddComponent(schemaId, propertiesSchema);
return new OpenApiSchemaReference(schemaId, document);
}
private static string GetPropertiesModelSchemaId(ContentTypeSchemaInfo contentType, PublishedItemType itemType) =>
$"{contentType.SchemaId}{GetItemTypeSuffix(itemType, contentType.IsElement)}{PropertiesModelSuffix}";
private string GetCompositionPropertiesModelSchemaId(string compositionSchemaId, PublishedItemType itemType)
{
// Look up the composition's own IsElement so its reference points at the right
// generated schema (element-type compositions live under the Element suffix).
IReadOnlyCollection<ContentTypeSchemaInfo> candidates = itemType == PublishedItemType.Media ? MediaTypes : DocumentTypes;
ContentTypeSchemaInfo? composition = candidates.FirstOrDefault(c => c.SchemaId == compositionSchemaId);
var suffix = GetItemTypeSuffix(itemType, composition?.IsElement ?? false);
return $"{compositionSchemaId}{suffix}{PropertiesModelSuffix}";
}
private static string GetItemTypeSuffix(PublishedItemType itemType, bool isElement) =>
itemType switch
{
PublishedItemType.Media => MediaSuffix,
PublishedItemType.Content => isElement ? ElementSuffix : ContentSuffix,
_ => throw new NotSupportedException($"Unsupported PublishedItemType: {itemType}"),
};
private async Task<Dictionary<string, IOpenApiSchema>> CreateContentTypeProperties(
ContentTypeSchemaInfo contentType,
OpenApiSchemaTransformerContext context,
CancellationToken cancellationToken)
{
var properties = new Dictionary<string, IOpenApiSchema>();
foreach (ContentTypePropertySchemaInfo propertyInfo in contentType.Properties.Where(p => !p.Inherited))
{
IOpenApiSchema schema = await CreateSchema(
GetJsonTypeInfo(propertyInfo.DeliveryApiClrType),
context,
cancellationToken);
// Properties may be null (e.g. property added after content was last published).
// Nullability is applied at the reference site, never on a shared component schema.
properties[propertyInfo.Alias] = AsNullable(schema);
}
return properties;
}
private JsonTypeInfo GetJsonTypeInfo(Type type)
{
JsonTypeInfo? jsonTypeInfo = _jsonTypeInfoResolver.GetTypeInfo(type, _serializerOptions);
return jsonTypeInfo ?? throw new InvalidOperationException("Could not get JsonTypeInfo for type " + type.FullName);
}
private string GetTypePropertyName(PublishedItemType itemType)
{
var propertyName = itemType switch
{
PublishedItemType.Content => nameof(IApiElement.ContentType),
PublishedItemType.Media => nameof(IApiMedia.MediaType),
_ => throw new NotSupportedException($"Unsupported PublishedItemType: {itemType}"),
};
return _serializerOptions.PropertyNamingPolicy?.ConvertName(propertyName) ?? propertyName;
}
private static string? GetSchemaId(JsonTypeInfo type)
=> ConfigureUmbracoOpenApiOptionsBase.CreateSchemaReferenceId(type);
/// <summary>
/// Creates a temporary placeholder schema to break circular reference chains during schema generation.
/// </summary>
/// <remarks>
/// The placeholder contains metadata with the target schema ID. During the document transformation phase,
/// <see cref="ResolveSchemaReferences(OpenApiDocument, IOpenApiSchema)"/> will replace these placeholders with actual schema references.
/// </remarks>
/// <param name="schemaId">The ID of the schema this placeholder represents.</param>
/// <returns>A placeholder schema with metadata indicating the target schema reference.</returns>
private static OpenApiSchema GetPlaceholderSchema(string schemaId)
=> new()
{
Metadata = new Dictionary<string, object>
{
[RecursiveRefMetadataKey] = schemaId,
},
};
/// <summary>
/// Recursively resolves inline schemas into proper <c>$ref</c> references.
/// </summary>
/// <remarks>
/// This method is called during the document transformation phase (after all schemas have been generated).
/// It walks through all schema properties, allOf, oneOf, and anyOf collections, resolving two types of
/// inline schemas:
/// <list type="bullet">
/// <item>Circular reference placeholders created by <see cref="GetPlaceholderSchema"/> (marked with <c>x-recursive-ref</c>)</item>
/// <item>Componentized schemas that should be references (marked with <c>x-schema-id</c>)</item>
/// </list>
/// Each match is replaced with an <see cref="OpenApiSchemaReference"/> pointing to the actual schema in the document's components.
/// </remarks>
/// <param name="document">The OpenAPI document containing the registered schema components.</param>
/// <param name="schema">The schema to process (will be modified in place).</param>
private static void ResolveSchemaReferences(OpenApiDocument document, IOpenApiSchema schema)
{
// Replace in allOf, oneOf, anyOf
ResolveSchemaReferences(document, schema.AllOf);
ResolveSchemaReferences(document, schema.OneOf);
ResolveSchemaReferences(document, schema.AnyOf);
// Process array items
if (schema is OpenApiSchema { Items: OpenApiSchema itemsSchema } parentSchema)
{
parentSchema.Items = GetActualSchemaOrReference(document, itemsSchema, out var itemsReplaced);
if (!itemsReplaced)
{
ResolveSchemaReferences(document, itemsSchema);
}
}
if (schema.Properties is not { Count: > 0 })
{
return;
}
// Process properties
foreach (var propertyKey in schema.Properties.Keys)
{
IOpenApiSchema propertySchema = schema.Properties[propertyKey];
if (propertySchema is not OpenApiSchema innerSchema)
{
continue;
}
schema.Properties[propertyKey] = GetActualSchemaOrReference(document, innerSchema, out var replaced);
if (replaced)
{
continue;
}
// Recursive call to handle the property schema
ResolveSchemaReferences(document, innerSchema);
}
}
private static void ResolveSchemaReferences(OpenApiDocument document, IList<IOpenApiSchema>? schemas)
{
if (schemas is null || schemas.Count == 0)
{
return;
}
for (var i = 0; i < schemas.Count; i++)
{
IOpenApiSchema allOfSchema = schemas[i];
schemas[i] = GetActualSchemaOrReference(document, allOfSchema, out var replaced);
if (!replaced)
{
ResolveSchemaReferences(document, schemas[i]);
}
}
}
[return: NotNullIfNotNull(nameof(schema))]
private static IOpenApiSchema? GetActualSchemaOrReference(
OpenApiDocument document,
IOpenApiSchema? schema,
out bool replaced)
{
if (schema is not OpenApiSchema openApiSchema)
{
replaced = false;
return schema;
}
// Check if this is a placeholder schema (circular reference)
if (openApiSchema.Metadata?.TryGetValue(RecursiveRefMetadataKey, out var recursiveRefIdObj) == true
&& recursiveRefIdObj is string recursiveRefId)
{
replaced = true;
return new OpenApiSchemaReference(recursiveRefId, document);
}
// Check if this is a componentized schema that should be a $ref
// Only resolve if the component actually exists — the framework also sets x-schema-id on
// schemas that may not end up as components.
if (openApiSchema.Metadata?.TryGetValue(SchemaIdMetadataKey, out var schemaIdObj) == true
&& schemaIdObj is string schemaId
&& !string.IsNullOrEmpty(schemaId)
&& document.Components?.Schemas?.ContainsKey(schemaId) == true)
{
replaced = true;
return new OpenApiSchemaReference(schemaId, document);
}
replaced = false;
return schema;
}
private IReadOnlyCollection<ContentTypeSchemaInfo> FilterAllowedDocumentTypes(IReadOnlyCollection<ContentTypeSchemaInfo> documentTypes)
{
DeliveryApiSettings settings = _deliveryApiSettings.CurrentValue;
return documentTypes
.Where(c => settings.IsAllowedContentType(c.Alias))
.ToList();
}
/// <summary>
/// Returns the schemas to use as the <c>allOf</c> bases for each typed content type
/// schema in a polymorphic union. Prefers concrete derived types declared on the
/// interface via <c>[JsonDerivedType]</c>; when none are advertised, falls back to a
/// schema built from the interface's own properties.
/// </summary>
/// <remarks>
/// The fallback exists for media interfaces, whose concrete classes are internal in
/// Umbraco.Infrastructure and therefore cannot be referenced via <c>[JsonDerivedType]</c>
/// from Umbraco.Core.
/// </remarks>
private async Task<List<IOpenApiSchema>> ResolveDerivedTypeSchemas(
OpenApiSchema interfaceSchema,
OpenApiSchemaTransformerContext context,
CancellationToken cancellationToken)
{
List<IOpenApiSchema> derivedTypeSchemas = [];
foreach (JsonDerivedType derivedType in context.JsonTypeInfo.PolymorphismOptions?.DerivedTypes ?? [])
{
IOpenApiSchema derivedTypeSchema = await CreateSchema(
GetJsonTypeInfo(derivedType.DerivedType),
context,
cancellationToken);
derivedTypeSchemas.Add(derivedTypeSchema);
}
if (derivedTypeSchemas.Count == 0)
{
derivedTypeSchemas.Add(CreateBaseSchemaFromInterface(interfaceSchema, context));
}
return derivedTypeSchemas;
}
private static IOpenApiSchema CreateBaseSchemaFromInterface(
OpenApiSchema interfaceSchema,
OpenApiSchemaTransformerContext context)
{
// Append a "Base" marker so this schema stays distinct from the polymorphic union
// schema for the same interface (e.g. IApiMediaWithCropsResponseBaseModel vs.
// IApiMediaWithCropsResponseModel).
var baseSchemaId = $"{context.JsonTypeInfo.Type.Name}Base{ModelSuffix}";
OpenApiDocument document = context.GetRequiredDocument();
var baseSchema = new OpenApiSchema
{
Type = interfaceSchema.Type,
Properties = interfaceSchema.Properties,
Required = interfaceSchema.Required,
Metadata = new Dictionary<string, object> { [SchemaIdMetadataKey] = baseSchemaId },
};
document.AddComponent(baseSchemaId, baseSchema);
return new OpenApiSchemaReference(baseSchemaId, document);
}
}
@@ -0,0 +1,104 @@
using Microsoft.AspNetCore.OpenApi;
using Microsoft.OpenApi;
namespace Umbraco.Cms.Api.Delivery.OpenApi.Transformers;
internal abstract class DeliveryApiTransformerBase : IOpenApiOperationTransformer
{
/// <summary>
/// Gets the link to the relevant documentation section.
/// </summary>
protected abstract string DocumentationLink { get; }
/// <inheritdoc/>
public async Task TransformAsync(
OpenApiOperation operation,
OpenApiOperationTransformerContext context,
CancellationToken cancellationToken)
{
if (!ShouldApply(context))
{
return;
}
AddExpand(operation);
AddFields(operation);
await ApplyAsync(operation, context, cancellationToken);
}
/// <summary>
/// Determines whether the transformer should be applied for the given context.
/// </summary>
/// <param name="context">The operation transformer context.</param>
/// <returns>>True if the transformer should be applied; otherwise, false.</returns>
protected abstract bool ShouldApply(OpenApiOperationTransformerContext context);
/// <summary>
/// Applies the specific transformations to the OpenAPI operation.
/// </summary>
/// <param name="operation">The <see cref="OpenApiOperation"/> to modify.</param>
/// <param name="context">The <see cref="OpenApiOperationTransformerContext"/> associated with the <see paramref="operation"/>.</param>
/// <param name="cancellationToken">The cancellation token to use.</param>
/// <returns>The task object representing the asynchronous operation.</returns>
protected abstract Task ApplyAsync(
OpenApiOperation operation,
OpenApiOperationTransformerContext context,
CancellationToken cancellationToken);
private void AddExpand(OpenApiOperation operation)
{
operation.Parameters ??= new List<IOpenApiParameter>();
operation.Parameters.Add(
new OpenApiParameter
{
Name = "expand",
In = ParameterLocation.Query,
Required = false,
Description = QueryParameterDescription("Defines the properties that should be expanded in the response"),
Schema = new OpenApiSchema { Type = JsonSchemaType.String },
Examples = new Dictionary<string, IOpenApiExample>
{
{ "Expand none", new OpenApiExample { Value = "" } },
{ "Expand all properties", new OpenApiExample { Value = "properties[$all]" } },
{ "Expand specific property", new OpenApiExample { Value = "properties[alias1]" } },
{ "Expand specific properties", new OpenApiExample { Value = "properties[alias1,alias2]" } },
{ "Expand nested properties", new OpenApiExample { Value = "properties[alias1[properties[nestedAlias1,nestedAlias2]]]" } },
},
});
}
private void AddFields(OpenApiOperation operation)
{
operation.Parameters ??= new List<IOpenApiParameter>();
operation.Parameters.Add(
new OpenApiParameter
{
Name = "fields",
In = ParameterLocation.Query,
Required = false,
Description =
QueryParameterDescription("Explicitly defines which properties should be included in the response (by default all properties are included)"),
Schema = new OpenApiSchema { Type = JsonSchemaType.String },
Examples = new Dictionary<string, IOpenApiExample>
{
{ "Include all properties", new OpenApiExample { Value = "properties[$all]" } },
{ "Include only specific property", new OpenApiExample { Value = "properties[alias1]" } },
{ "Include only specific properties", new OpenApiExample { Value = "properties[alias1,alias2]" } },
{ "Include only specific nested properties", new OpenApiExample { Value = "properties[alias1[properties[nestedAlias1,nestedAlias2]]]" } },
},
});
}
protected void AddQueryParameterDocumentation(OpenApiParameter parameter, Dictionary<string, IOpenApiExample> examples, string description)
{
parameter.Description = QueryParameterDescription(description);
parameter.Examples = examples;
}
protected string PaginationDescription(bool skip, string itemType)
=> $"Specifies the number of found {itemType} items to {(skip ? "skip" : "take")}. Use this to control pagination of the response.";
private string QueryParameterDescription(string description)
=> $"{description}. Refer to [the documentation]({DocumentationLink}#query-parameters) for more details on this.";
}
@@ -1,27 +1,41 @@
using System.Text.Json.Nodes;
using System.Text.Json.Nodes;
using Microsoft.AspNetCore.Mvc.Controllers;
using Microsoft.AspNetCore.OpenApi;
using Microsoft.OpenApi;
using Swashbuckle.AspNetCore.SwaggerGen;
using Umbraco.Cms.Api.Delivery.Configuration;
using Umbraco.Cms.Api.Delivery.Controllers.Media;
using Umbraco.Extensions;
namespace Umbraco.Cms.Api.Delivery.Filters;
namespace Umbraco.Cms.Api.Delivery.OpenApi.Transformers;
internal sealed class SwaggerMediaDocumentationFilter : SwaggerDocumentationFilterBase<MediaApiControllerBase>
/// <summary>
/// Transforms OpenAPI operations for the Media API, adding relevant parameters and documentation.
/// </summary>
internal sealed class MediaApiTransformer : DeliveryApiTransformerBase
{
protected override string DocumentationLink => DeliveryApiConfiguration.ApiDocumentationMediaArticleLink;
protected override void ApplyOperation(OpenApiOperation operation, OperationFilterContext context)
/// <inheritdoc/>
protected override bool ShouldApply(OpenApiOperationTransformerContext context) =>
context.Description.ActionDescriptor is ControllerActionDescriptor description
&& description.ControllerTypeInfo.Implements<MediaApiControllerBase>();
/// <inheritdoc/>
protected override Task ApplyAsync(
OpenApiOperation operation,
OpenApiOperationTransformerContext context,
CancellationToken cancellationToken)
{
operation.Parameters ??= new List<IOpenApiParameter>();
foreach (OpenApiParameter parameter in operation.Parameters?.OfType<OpenApiParameter>() ?? [])
{
ApplyParameter(parameter);
}
AddExpand(operation, context);
AddFields(operation, context);
AddApiKey(operation);
return Task.CompletedTask;
}
protected override void ApplyParameter(OpenApiParameter parameter, ParameterFilterContext context)
private void ApplyParameter(OpenApiParameter parameter)
{
switch (parameter.Name)
{
@@ -56,18 +70,18 @@ internal sealed class SwaggerMediaDocumentationFilter : SwaggerDocumentationFilt
private Dictionary<string, IOpenApiExample> FilterQueryParameterExamples() =>
new()
{
{ "Default filter", new OpenApiExample { Value = string.Empty } },
{ "Filter by media type", new OpenApiExample { Value = new JsonArray { "mediaType:alias1" } } },
{ "Filter by name", new OpenApiExample { Value = new JsonArray { "name:nodeName" } } },
{ "Default filter", new OpenApiExample { Value = new JsonArray(string.Empty) } },
{ "Filter by media type", new OpenApiExample { Value = new JsonArray("mediaType:alias1") } },
{ "Filter by name", new OpenApiExample { Value = new JsonArray("name:nodeName") } },
};
private Dictionary<string, IOpenApiExample> SortQueryParameterExamples() =>
new()
{
{ "Default sort", new OpenApiExample { Value = string.Empty } },
{ "Sort by create date", new OpenApiExample { Value = new JsonArray { "createDate:asc", "createDate:desc" } } },
{ "Sort by name", new OpenApiExample { Value = new JsonArray { "name:asc", "name:desc" } } },
{ "Sort by sort order", new OpenApiExample { Value = new JsonArray { "sortOrder:asc", "sortOrder:desc" } } },
{ "Sort by update date", new OpenApiExample { Value = new JsonArray { "updateDate:asc", "updateDate:desc" } } },
{ "Default sort", new OpenApiExample { Value = new JsonArray(string.Empty) } },
{ "Sort by create date", new OpenApiExample { Value = new JsonArray("createDate:asc", "createDate:desc") } },
{ "Sort by name", new OpenApiExample { Value = new JsonArray("name:asc", "name:desc") } },
{ "Sort by sort order", new OpenApiExample { Value = new JsonArray("sortOrder:asc", "sortOrder:desc") } },
{ "Sort by update date", new OpenApiExample { Value = new JsonArray("updateDate:asc", "updateDate:desc") } },
};
}
@@ -0,0 +1,53 @@
using Microsoft.AspNetCore.OpenApi;
using Microsoft.OpenApi;
using Umbraco.Cms.Api.Common.Security;
namespace Umbraco.Cms.Api.Delivery.OpenApi.Transformers;
/// <summary>
/// Transformer that adds member authentication security requirements to OpenAPI documents.
/// </summary>
internal class MemberAuthenticationSecurityRequirementsTransformer : IOpenApiOperationTransformer, IOpenApiDocumentTransformer
{
private const string AuthSchemeName = "UmbracoMember";
/// <inheritdoc />
public Task TransformAsync(
OpenApiDocument document,
OpenApiDocumentTransformerContext context,
CancellationToken cancellationToken)
{
var securityScheme = new OpenApiSecurityScheme
{
In = ParameterLocation.Header,
Name = AuthSchemeName,
Type = SecuritySchemeType.OAuth2,
Description = "Umbraco Member Authentication",
Flows = new OpenApiOAuthFlows
{
AuthorizationCode = new OpenApiOAuthFlow
{
AuthorizationUrl = new Uri(Paths.MemberApi.AuthorizationEndpoint, UriKind.Relative),
TokenUrl = new Uri(Paths.MemberApi.TokenEndpoint, UriKind.Relative),
},
},
};
document.AddComponent(AuthSchemeName, securityScheme);
return Task.CompletedTask;
}
/// <inheritdoc />
public Task TransformAsync(
OpenApiOperation operation,
OpenApiOperationTransformerContext context,
CancellationToken cancellationToken)
{
var schemaRef = new OpenApiSecuritySchemeReference(AuthSchemeName, context.Document);
operation.Security ??= new List<OpenApiSecurityRequirement>();
operation.Security.Add(new OpenApiSecurityRequirement { [schemaRef] = [] });
return Task.CompletedTask;
}
}
@@ -30,7 +30,7 @@ internal sealed class DeliveryApiItemsEndpointsMatcherPolicy : MatcherPolicy, IE
public Task ApplyAsync(HttpContext httpContext, CandidateSet candidates)
{
var hasIdQueryParameter = httpContext.Request.Query.ContainsKey("id");
ApiVersion? requestedApiVersion = httpContext.GetRequestedApiVersion();
ApiVersion? requestedApiVersion = httpContext.RequestedApiVersion;
for (var i = 0; i < candidates.Count; i++)
{
CandidateState candidate = candidates[i];
@@ -18,6 +18,7 @@ internal sealed class RequestRedirectService : RoutingServiceBase, IRequestRedir
private readonly IRedirectUrlService _redirectUrlService;
private readonly IApiPublishedContentCache _apiPublishedContentCache;
private readonly IApiContentRouteBuilder _apiContentRouteBuilder;
private readonly IDocumentUrlService _documentUrlService;
private readonly GlobalSettings _globalSettings;
public RequestRedirectService(
@@ -28,13 +29,15 @@ internal sealed class RequestRedirectService : RoutingServiceBase, IRequestRedir
IRedirectUrlService redirectUrlService,
IApiPublishedContentCache apiPublishedContentCache,
IApiContentRouteBuilder apiContentRouteBuilder,
IOptions<GlobalSettings> globalSettings)
IOptions<GlobalSettings> globalSettings,
IDocumentUrlService documentUrlService)
: base(domainCache, httpContextAccessor, requestStartItemProviderAccessor)
{
_requestCultureService = requestCultureService;
_redirectUrlService = redirectUrlService;
_apiPublishedContentCache = apiPublishedContentCache;
_apiContentRouteBuilder = apiContentRouteBuilder;
_documentUrlService = documentUrlService;
_globalSettings = globalSettings.Value;
}
@@ -43,16 +46,19 @@ internal sealed class RequestRedirectService : RoutingServiceBase, IRequestRedir
requestedPath = requestedPath.EnsureStartsWith("/");
IPublishedContent? startItem = GetStartItem();
var culture = _requestCultureService.GetRequestedCulture();
// must append the root content url segment if it is not hidden by config, because
// the URL tracking is based on the actual URL, including the root content url segment
if (_globalSettings.HideTopLevelNodeFromPath == false && startItem?.UrlSegment != null)
if (_globalSettings.HideTopLevelNodeFromPath == false && startItem is not null)
{
requestedPath = $"{startItem.UrlSegment.EnsureStartsWith("/")}{requestedPath}";
var startItemUrlSegment = _documentUrlService.GetUrlSegment(startItem.Key, culture ?? string.Empty, isDraft: false);
if (startItemUrlSegment is not null)
{
requestedPath = $"{startItemUrlSegment.EnsureStartsWith("/")}{requestedPath}";
}
}
var culture = _requestCultureService.GetRequestedCulture();
// important: redirect URLs are always tracked without trailing slashes
requestedPath = requestedPath.TrimEnd("/");
IRedirectUrl? redirectUrl = _redirectUrlService.GetMostRecentRedirectUrl(requestedPath, culture);
@@ -3,7 +3,7 @@ using Umbraco.Cms.Core.DeliveryApi;
namespace Umbraco.Cms.Api.Delivery.Services;
internal sealed class RequestSegmentService : RequestHeaderHandler, IRequestSegmentService, IRequestSegmmentService
internal sealed class RequestSegmentService : RequestHeaderHandler, IRequestSegmentService
{
public RequestSegmentService(IHttpContextAccessor httpContextAccessor)
: base(httpContextAccessor)
@@ -3,6 +3,7 @@ using Umbraco.Cms.Core;
using Umbraco.Cms.Core.DeliveryApi;
using Umbraco.Cms.Core.Models.PublishedContent;
using Umbraco.Cms.Core.PublishedCache;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Services.Navigation;
using Umbraco.Extensions;
@@ -14,6 +15,7 @@ internal sealed class RequestStartItemProvider : RequestHeaderHandler, IRequestS
private readonly IRequestPreviewService _requestPreviewService;
private readonly IDocumentNavigationQueryService _documentNavigationQueryService;
private readonly IPublishedContentCache _publishedContentCache;
private readonly IDocumentUrlService _documentUrlService;
// this provider lifetime is Scope, so we can cache this as a field
private IPublishedContent? _requestedStartContent;
@@ -23,14 +25,15 @@ internal sealed class RequestStartItemProvider : RequestHeaderHandler, IRequestS
IVariationContextAccessor variationContextAccessor,
IRequestPreviewService requestPreviewService,
IDocumentNavigationQueryService documentNavigationQueryService,
IPublishedContentCache publishedContentCache)
IPublishedContentCache publishedContentCache,
IDocumentUrlService documentUrlService)
: base(httpContextAccessor)
{
_variationContextAccessor = variationContextAccessor;
_requestPreviewService = requestPreviewService;
_documentNavigationQueryService = documentNavigationQueryService;
_publishedContentCache = publishedContentCache;
_documentUrlService = documentUrlService;
}
/// <inheritdoc/>
@@ -47,14 +50,16 @@ internal sealed class RequestStartItemProvider : RequestHeaderHandler, IRequestS
return null;
}
var isPreview = _requestPreviewService.IsPreview();
_documentNavigationQueryService.TryGetRootKeys(out IEnumerable<Guid> rootKeys);
IEnumerable<IPublishedContent> rootContent = rootKeys
.Select(rootKey => _publishedContentCache.GetById(_requestPreviewService.IsPreview(), rootKey))
.Select(rootKey => _publishedContentCache.GetById(isPreview, rootKey))
.WhereNotNull();
var culture = _variationContextAccessor.VariationContext?.Culture ?? string.Empty;
_requestedStartContent = Guid.TryParse(headerValue, out Guid key)
? rootContent.FirstOrDefault(c => c.Key == key)
: rootContent.FirstOrDefault(c => c.UrlSegment(_variationContextAccessor).InvariantEquals(headerValue));
: rootContent.FirstOrDefault(c => _documentUrlService.GetUrlSegment(c.Key, culture, isPreview).InvariantEquals(headerValue));
return _requestedStartContent;
}

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