Compare commits

...
Author SHA1 Message Date
Andy ButlandandLaura Neto c9c16d2605 URL Info: Fix invariant content URLs missing under non-default language domains (closes #21866) (#21883)
* Show correct URLs for invariant content under non-default language domains.

* Use configured domain hosts instead of request host for fallback URL filtering.

* Addressed feedback from code review.

* Fixed code warnings.

* Update file references in integration test csproj.

* Simplify invariant URL culture filtering by determining cultures upfront

Instead of querying all cultures and post-processing to remove irrelevant
URLs, determine the relevant cultures before the loop by checking which
domains are assigned to the content's ancestor path.

---------

Co-authored-by: Laura Neto <12862535+lauraneto@users.noreply.github.com>
2026-02-25 11:28:39 +01:00
Andy Butland 62eb91675d Database Cache: Fix full database cache rebuild dropping variant and composed property values (closes #21863, #21882) (#21890)
* Resolve full database cache rebuild dropping variant and composed property values

* Addressed feedback from code review.
2026-02-25 08:01:25 +01:00
Andy Butland 79b3058a96 Bump version to 17.2.1. 2026-02-23 16:39:49 +01:00
Andy Butland 51e91c88ae Bump version to 17.2.0. 2026-02-19 06:49:24 +01:00
Jacob OvergaardandGitHub 725a0322ed build(deps): bumps @umbraco-ui/uui to 1.17.0 (#21765) 2026-02-18 14:59:45 +01:00
b1ef0153f3 Block Workspace: Inline Editing Workspace to awaits all Content Type compositions before setting initial active tab (#21719)
* fix contentTypeLoaded reaction in Block Workspace without a router

* Update src/Umbraco.Web.UI.Client/src/packages/block/block/workspace/views/edit/block-workspace-view-edit-content-no-router.element.ts

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

* revert check with todo comment

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-02-11 11:25:45 +00:00
Niels LyngsøandGitHub bfee99c5b0 Block Workspace: Tabs navigation, Cherry-pick from #21672 (#21693)
* cherry-pick from #21672

* cherry pick tab rendering to handle one more case

* move the root route down for it to stay an empty path.

* Revert empty root path commit

* fullPath for root includes 'root'

* revert claude settings commit

* refactor accordingly to feedback
2026-02-10 14:17:14 +00:00
Andy Butland ed963b2dbb Bumped version to 17.2.0-rc2. 2026-02-10 15:16:52 +01:00
Niels LyngsøandGitHub 79dfe76286 Block Workspace: renme root-tab to 'generic' (#21699)
* renme to generic

* only use label
2026-02-10 13:31:01 +00:00
Andy ButlandandLan Nguyen Thuy 13fb4a4e09 OEmbed providers: Tighten up resource URL matching for providers (#21583)
* edit regex for oembed flickr

* Apply stricter matching with domain to all embed providers, and validate with unit tests.

* Resolved warnings and added further unit tests.

* Further tightened the URL matching regex for two providers.

* Add regex caching to OEmbedService and unit tests to verify behaviour.

* Restore flickr short URL domain.

* Use https in requests to oembed providers.

---------

Co-authored-by: Lan Nguyen Thuy <lnt@umbraco.dk>
Co-authored-by: Andy Butland <abutland73@gmail.com>
2026-02-05 07:14:43 +01:00
2917e5be97 Routing: Fix URL aliases not stored for variant content with shared alias property in DocumentUrlAliasService (#21571)
* Fix issue where URL aliases on variant content with a shared property were not being recorded.

* Apply suggestions from code review

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

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-02-04 09:54:20 +01:00
b99547db50 Documents: Remove deprecated entityType from property values (closes #21567) (#21609)
Documents: Remove deprecated entityType from property values

The entityType property on property values was causing "Unsaved Changes"
modal to appear after saving documents with RTE blocks. This occurred
because the server data source added entityType when reading, but
setPropertyValue did not preserve it when updating values.

Since entityType on UmbElementValueModel is deprecated and marked for
removal in v18, the cleanest fix is to stop adding it in the server
data source mapping.

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-03 14:18:10 +01:00
3ab7a03254 Content Editor: Fix display of validation hint badge on tabs (#21595)
* Fix display of validation hint related to a tab.

* Update position of the badge.

* Change position for last tab.

---------

Co-authored-by: engjlr <enl@umbraco.dk>
Co-authored-by: Engiber Lozada <89547469+engijlr@users.noreply.github.com>
2026-02-03 10:13:50 +00:00
Jacob OvergaardandGitHub 6aeffe7a6e build(deps): bumps @umbraco-ui/uui to 1.17.0-rc.5 (#21569) 2026-02-02 12:40:30 +00:00
Andy ButlandandGitHub 574c9bed6c Content Types: Fix deletion of properties without containers (closes #21566) (#21585)
Fix issue with deleting properties that are not in containers.
2026-01-30 14:41:09 +01:00
Jacob Overgaard ae10b6685a generates api types 2026-01-29 12:12:31 +01:00
Niels LyngsøandGitHub c1b0672ce0 Content Value Transformation: Clean out values when property-type variation transforms (#21557)
* cleanup values of property when property type variation changes

* implement handling segments in transformation
2026-01-28 14:16:13 +01:00
8efdfcd341 Backoffice: Exclude invariant options for culture-variant properties in preset builder (#21555)
* Exclude invariant options for culture-variant properties in preset builder

* Add unit test verifying the fix.

* added a few more unit tests

---------

Co-authored-by: Niels Lyngsø <niels.lyngso@gmail.com>
2026-01-28 11:30:54 +00:00
3c7a1ad2de Entity Signs: Refactor Entity Sign Bundle to use Popover API. (#21490)
* Refactor entity sign tooltip to use popover API.

* Refactor popover positioning and styling logic.

* Add preview icons to entity sign bundle.

* Improve entity sign preview rendering and popover state.

* Refactor entity sign popover and sign container styles.

* Remove unused index parameter.

* Update menu item background color styles.

* Revert commented lines.

* Refactor entity sign popover rendering logic.

* Refactor popover sign creation into separate method.

* keep previews on hover

---------

Co-authored-by: Mads Rasmussen <madsr@hey.com>
2026-01-28 10:37:20 +00:00
5485a31f75 Tiptap RTE: Resolves inline blocks being set as dirty (closes #17749) (#21546)
* Resolves RTE inline blocks being flagged as dirty

Fixes #17749

* Deprecated `displayInline` field

* Update src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/block/block.tiptap-api.ts

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

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-01-28 09:52:45 +00:00
Jacob OvergaardandGitHub 345a075706 Dependencies: Bumps @umbraco-ui/uui to 1.17.0-rc.4 (#21538)
* build(deps): bumps @umbraco-ui/uui to 1.17.0-rc.2

* build(deps-dev): bumps @umbraco-ui/uui to rc.3 to fix deps mess

* build(deps): bumps @umbraco-ui/uui to 1.17.0-rc.4
2026-01-28 09:52:08 +00:00
Niels LyngsøandGitHub 046c7d207d Content Type Designer: Property Layout updates (#21544)
* remove alias id

* adjust spacing and sizing for improved space in the layout
2026-01-28 09:26:37 +00:00
a5a67a4381 Translations: Missing translations in user permission (#21541)
* Fix issue localization of user permission

* add localize to create button

---------

Co-authored-by: Lan Nguyen Thuy <lnt@umbraco.dk>
2026-01-28 10:13:39 +01:00
Andy ButlandandGitHub e259cd0fd3 External Logins: Handle duplicate key race condition intermittently triggered in ExternalLoginRepository (#21551)
Handle duplicate key race condition in ExternalLoginRepository.
2026-01-28 06:27:27 +01:00
bohdansolovieandGitHub b5559eb9e8 View Engines: Make ProfilingViewEngine._inner private and modernize string formatting (#21550)
improvement(web): make ProfilingViewEngine._inner private and modernize string formatting

- Changed internal readonly Inner field to private readonly _inner field
- Replaced string.Format calls with string interpolation
- Removed TODO comment
2026-01-27 17:51:24 +01:00
f2c351df64 Skip leading whitespace in ufm parser (#21509)
* Skip leading whitespace in ufm parser

* UFM: Update start function to also skip leading whitespace

The tokenizer was updated to allow whitespace after opening braces,
but the start function still used a string pattern without whitespace
tolerance. This updates start to use a pre-compiled regex that matches
the tokenizer behavior, and adds an additional test case for the
documentation example format.

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

---------

Co-authored-by: leekelleher <leekelleher@gmail.com>
2026-01-27 16:02:27 +00:00
Andy Butland 4e6481abd9 Bumped test dependencies to latest minor or patch. 2026-01-27 15:30:43 +01:00
Andy Butland b40c17582a Bump dependency on SixLabors.ImageSharp for ImageSharp2. 2026-01-27 15:30:13 +01:00
Lee KelleherandGitHub af7a21723d Tiptap RTE: Upgraded to latest v3.x (#21493)
* Upgraded Tiptap to v3.13.0

* Remove eslint disable comments

* Update notes in externals

* `TextDirection` is now part of Tiptap core

* Upgraded Tiptap to v3.16.0

* The `addOptions()` typing error still persists in v3.16.0

* Resolved the export issue

* Removed unrequired `@ts-expect-error`

This came from an upstream merge.
2026-01-27 12:04:48 +00:00
Jacob OvergaardandGitHub fc5a8a0536 build(deps-dev): bumps login dependencies to latest (#21539) 2026-01-27 11:39:33 +01:00
7d813667c3 Content/Media: Fix deadlock when performing certain operations in parallel (closes #21125) (#21526)
* Move MediaTree write lock before MediaSavingNotification to prevent deadlock

Fixes a deadlock that could occur when saving multiple media items in parallel
when a MediaSavingNotification handler acquires a MediaTree read lock. The
previous ordering allowed two threads to each acquire read locks in their
notification handlers, then both attempt to upgrade to write locks, causing
a classic lock upgrade deadlock in SQL Server.

By acquiring the write lock before publishing the notification, the deadlock
scenario is avoided. Since the write lock is lazy, it only materializes at the
database level when actual queries are made, so notification handlers doing
in-memory work won't hold the lock.

* Apply same fix to MediaService.Delete method

* Apply same fix to DeleteVersions, DeleteVersion, and Sort methods

* Apply same fix to ContentService methods

Move WriteLock before notifications in:
- Save (single and batch)
- Delete
- DeleteVersions
- DeleteVersion
- Copy

* Apply the same pattern to MemberService.

* Add integration tests to verify the fix.

---------

Co-authored-by: Andy Butland <abutland73@gmail.com>
2026-01-27 11:08:35 +01:00
Andy ButlandandGitHub 138818acde Members: Fix misleading error message on change password with incorrect current password (#21504)
* Provide correct validation error message on member change password with incorrect current password.

* Rework to use MembersErrorDescriber.
2026-01-27 10:37:28 +01:00
Nhu DinhandGitHub 79753eaf82 E2E: QA Updated acceptance tests for collection view search and document type group (#21522) 2026-01-27 16:00:12 +07:00
Andy ButlandandGitHub 647aa08586 Dependencies: Bump to latest minor/patch versions (#21540)
Bump dependencies to the latest minor or patch.
2026-01-27 08:34:18 +00:00
Nhu DinhandGitHub 9deffead21 E2E: QA Added acceptance tests for multi url picker validation message (#21226)
* Added tests for multi url picker validation message

* Added more tests - not done

* Updated more tests for multi url picker validation message

* Removed unused file

* Bumped version

* Make tests run in the pipeline

* Reverted npm command
2026-01-27 08:31:41 +00:00
Andy ButlandandGitHub d5d93ff1e0 Templates: Allow underscore as first character in template alias (closes #21534) (#21536)
Use a custom regex for validating template aliases that allows underscores.
2026-01-27 08:32:37 +01:00
7ad3d2f68f Content picker: Fix bug where dynamic root children are not correctly available for selection (closes #21477 and #21537) (#21535)
Fix bug content picker dynamic root children not selected properly

Co-authored-by: Lan Nguyen Thuy <lnt@umbraco.dk>
2026-01-27 08:27:52 +01:00
Andreas ZerbstandGitHub 2fb1221d68 Dotnet Template: Fix trailing comma in appsettings.json (#21529)
Fix trailing comma in global settings
2026-01-27 03:34:18 +00:00
efb8aaf87b Dark mode: Added color variable to code block in the system information dialog to make it readable (#21532)
* added color variable to code-block to make it readable in dark mode

* Update src/Umbraco.Web.UI.Client/src/packages/core/components/code-block/code-block.element.ts

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

---------

Co-authored-by: Andy Butland <abutland73@gmail.com>
2026-01-26 18:07:17 +00:00
2e80b996cf Media: Prevent creation of media with GUID v7 keys when using incompatible path scheme (closes #21440) (#21457)
* Prevent creation of media items with GUID version 7 keys when a media scheme is registered that doesn't support this GUID version.

* Apply suggestions from code review

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

* Fix log message formatting.

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Kenn Jacobsen <kja@umbraco.dk>
2026-01-26 16:29:37 +01:00
Andy ButlandandGitHub 883b6a4d5d Models Builder: Fix nested generic type handling in WriteClrType (#21429)
* Add support to models builder for nested generic types.

* Fixed existing warnings, added further tests, renamed tests for clarity.

* Add defensive validation for generic brackets passed to SplitGenericArguments.

* Fix failing unit tests.
2026-01-26 15:32:48 +01:00
6946783b74 Content types: Allow adding composition with clashing property alias when property is being removed (closes #21298) (#21527)
* Content types: Allow adding composition with clashing property alias when property is being removed

* Further assert on property coming from the composition.

---------

Co-authored-by: Andy Butland <abutland73@gmail.com>
2026-01-26 14:32:34 +00:00
ad759e9311 Block editors: Fix false pending changes indicator for invariant block editor with culture-variant blocks (closes #21223) (#21292)
* Block editors: Fix false pending changes indicator for invariant BlockList with culture-variant blocks (closes #21223)

When a document with a culture-variant content type has an invariant BlockList property containing culture-variant blocks, and you publish all languages for the first time, the content would incorrectly show as having unpublished changes.

The root cause was inconsistent JSON serialization order between EditedValue and PublishedValue. Two fixes were applied:

1. Sort block item values by culture before serialization in both `FromEditor` and `MergePartialPropertyValueForCulture` to ensure consistent ordering.

2. Add `[JsonIgnore]` to `BlockItemData.Udi` property since this computed property differs between save and publish paths.

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

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

* Update tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/PropertyEditors/BlockListElementLevelVariationTests.Publishing.cs

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

* Fixed failing integration tests.

* Fix backwards compatibility for legacy UDI format in JSON deserializatio

* Tidy up, remove unused parameters.

* Fixed failing E2E test with copy blocks.

* Separate handling of udi and values in deserialization from current and legacy format, to correctly fix previously failing integeration and E2E tests.

* Fixed failing unit test.

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-01-26 14:51:16 +01:00
00794f48c5 Tiptap RTE: Adds link (umbLink) support to styleMenu API (#21494)
* Adds link (`umbLink`) support to the Style Menu api

* Tiptap RTE: Fix toggleClassName to handle multi-class strings

The toggleClassName command now properly tokenizes the className parameter
to handle space-separated classes (e.g., "btn btn-primary"). Previously,
the entire string was treated as a single token, causing duplicates and
preventing class removal.

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

* Tiptap RTE: Add ensureUmbLink command for idempotent link creation

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

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-26 14:24:29 +01:00
Niels LyngsøandGitHub 2c5466755a Block List Editor: Describe that Single Block Mode is deprecated (#21512)
mark Single Block Mode as deprecated
2026-01-26 13:07:54 +00:00
DreamandGitHub 7438dd7c84 Backoffice: Redirect to list view after entity deletion (#21456)
Backoffice: Redirect to list view after entity deletion

When an entity is deleted from its detail workspace, the UI now redirects
to the parent list view and shows a success notification instead of staying
on the deleted entity's page showing a 404 error.

Changes:
- Dispatch UmbEntityDeletedEvent after successful deletion
- Show success notification toast on deletion
- Listen for delete event in workspace editor and navigate to backPath
2026-01-26 12:54:25 +00:00
Andy ButlandandGitHub 868ac50b79 Media: Only add deleted suffix to URLs for trashed media when recycle bin protection is enabled (#21412)
Fix protection for media URLs such that they only apply for trashed media.
2026-01-26 13:41:01 +01:00
Sven GeusensandGitHub 897b6ccac6 Load Balancing: Tracking difference CM and CD redirect and post-logout URIs in load-balanced environments (#21432)
* Integration tests for #21138

* Make OpenId redirect and postlogout uris support load balanced environments

* Applied review suggestions

* Fix unit test mocks
2026-01-26 12:08:42 +01:00
997df3d92b Performance: Optimize property retrieval and authorization checks in collection views (#21470)
* Introduce new method overloads and repository implentation, such that a collection view response only loads properties it needs.

* Use non-obsolete method overloads throughout.

* Add unit tests to verify property value retrieval.

* Don't load templates for collection view content retrieval.

* Optimize access checks by verifying the full collection rather than one at a time, and avoid the need to retrieve full content items.

* Added obsoletion messages and aligned behaviour of content and media permission service checks.

* Apply suggestions from code review

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

* Return key in TreeEntityPath collection response, avoiding a later look-up of the key by Id.

* Resolve breaking changes to interfaces.

* Fix further breaking change.

* Additional assert for test verifying property loading for a non-existing property.

* Refactored repositories to avoid having method parameters related to templates on non-document and base content repositories.

* Remove check that verifies all provided keys are found when doing permission checks (although arguably correct, it's a behavioural change, and can also be argued it's corect as is).

* Introduce variable for permission set permissions.

* Provide functional default implementation on FilterAuthorizedAsync.

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-01-26 11:53:53 +01:00
e4df87123c Testing: Ensure ordering for paged descendants tests (closes #21446) (#21447)
* Squash merge Squash merged v173/20453-fix-more-sql-syntax-issues int v173/20453-fix-more-sql-syntax-issues-squash (copy of main)

* fix 2 unit test

* Replace nameof(DTO.COLUMN_NAME) by constant, because it leads to casing issues for case sensitive databses

* fix Copilot review comments

* resolve review comments

* replace more hard coded strings

* fix test

* fix review comments

* fix database schema

* fix database schema

* fix database schema and ResultColumn reference names

* Update src/Umbraco.Infrastructure/Persistence/Dtos/ContentTypeAllowedContentTypeDto.cs

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

* add comment  from review

* fix two reference column names

* fix breaking change

* fix typo

* Remove unnecessary attributes

* mark 2 unsused DTO classes as obsolete

* reverted change of class UnionHelperDto adding [Column("...")] attributes again, because some integration tests for PostgreSQL provider fail without them. Again a case sensitivty issue.

* replace nameof reference names,
make all column name const consistent

* use NPoco dto instead of raw sql,
extend ISqlSyntaxProvider to handle some sql issues

* reduce complexity

* remove currently unsused extensions to ISqlSyntax

* add missing methods to ISqlSyntaxProvider and SqlSyntaxProviderBase

* add another missing methods to ISqlSyntaxProvider and SqlSyntaxProviderBase

* fix Copilot review comments and build errors

* update ISqlSyntaxProvider and SqlSyntaxProviderBase

* ensure GetPagedDescendants returns ordered by path entities as default

* resolve review comments

* fix review comments

* Update src/Umbraco.Infrastructure/Persistence/SqlSyntax/ISqlSyntaxProvider.cs

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

* Update src/Umbraco.Infrastructure/Migrations/Install/DatabaseSchemaCreator.cs

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

* Update src/Umbraco.PublishedCache.HybridCache/Persistence/DatabaseCacheRepository.cs

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

* Update src/Umbraco.Infrastructure/Persistence/SqlSyntax/ISqlSyntaxProvider.cs

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

* fix Copilot comment

* fix wrong Copilot suggestion

* quote more column names

* resolve review

* Apply suggestions from code review

* synced interface and base class

* Revert "synced interface and base class". For an interface's default implementation, NotImplementedException makes more sense.

This reverts commit cf01cd01fc.

* Fixed remaining code warnings in DatabaseSchemaCreator.

* follow Cotpilot's review suggestion

* revert implementation and fix test

* use default

---------

Co-authored-by: Andy Butland <abutland73@gmail.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-01-26 09:36:13 +00:00
c6370e39a7 Recycle Bin: Adds emptyRecycleBin collection action kind for Documents and Media (#21482)
* Adds reusable `emptyRecycleBin` `collectionAction` kind

* Adds `emptyRecycleBin` collection-action to documents

* Adds `emptyRecycleBin` collection-action to media

* Removes `api` export

since the condition is eagerly loaded.

* Fixes type annotations and JSDoc comments

- Uses correct generic type `UmbCollectionHasItemsConditionConfig` in `UmbCollectionHasItemsCondition`
- Corrects JSDoc `@augments` tag in `UmbEmptyRecycleBinCollectionAction`

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

* Fixed linting errors

* Refactors execute() to reduce cyclomatic complexity

Extracts tree refresh logic into private #reloadChildrenOfEntity() method.

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

* Update src/Umbraco.Web.UI.Client/src/packages/media/media/recycle-bin/manifests.ts

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

* Removed code comment

as caused ambiguity.

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Co-authored-by: Andy Butland <abutland73@gmail.com>
2026-01-26 09:25:37 +00:00
08863332d2 Disabled the generation and upload off the docfx csharp api docs. (#21521)
* Disabled the generation and upload off the docfx csharp api docs.

* Add comment explaining why job is disabled

* Added comment on second job

---------

Co-authored-by: Andy Butland <abutland73@gmail.com>
2026-01-26 09:07:00 +00:00
f483946c4d Code Quality: Added missing documentation to the Umbraco.Cms.Api.Common project (#21465)
* Added missing code documentation to the Umbraco.Cms.Api.Common project

* Remove duplicate XML summary for All constant

Removed duplicate XML summary documentation for the All constant.

* Removed inline comments no longer required now the information has been moved to XML header remarks

* Fix indentation on refactored path segment extraction in SubTypesSelector

---------

Co-authored-by: Andy Butland <abutland73@gmail.com>
2026-01-26 07:28:56 +00:00
e7624364ed Database Providers: Add support for providers offering sequence and null casting support (closes #21418) (#21419)
* Squash merge Squash merged v173/20453-fix-more-sql-syntax-issues int v173/20453-fix-more-sql-syntax-issues-squash (copy of main)

* fix 2 unit test

* Replace nameof(DTO.COLUMN_NAME) by constant, because it leads to casing issues for case sensitive databses

* fix Copilot review comments

* resolve review comments

* replace more hard coded strings

* fix test

* fix review comments

* fix database schema

* fix database schema

* fix database schema and ResultColumn reference names

* Update src/Umbraco.Infrastructure/Persistence/Dtos/ContentTypeAllowedContentTypeDto.cs

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

* add comment  from review

* fix two reference column names

* fix breaking change

* fix typo

* Remove unnecessary attributes

* mark 2 unsused DTO classes as obsolete

* reverted change of class UnionHelperDto adding [Column("...")] attributes again, because some integration tests for PostgreSQL provider fail without them. Again a case sensitivty issue.

* replace nameof reference names,
make all column name const consistent

* use NPoco dto instead of raw sql,
extend ISqlSyntaxProvider to handle some sql issues

* reduce complexity

* remove currently unsused extensions to ISqlSyntax

* add missing methods to ISqlSyntaxProvider and SqlSyntaxProviderBase

* add another missing methods to ISqlSyntaxProvider and SqlSyntaxProviderBase

* fix Copilot review comments and build errors

* update ISqlSyntaxProvider and SqlSyntaxProviderBase

* resolve review comments

* fix review comments

* Update src/Umbraco.Infrastructure/Persistence/SqlSyntax/ISqlSyntaxProvider.cs

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

* Update src/Umbraco.Infrastructure/Migrations/Install/DatabaseSchemaCreator.cs

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

* Update src/Umbraco.PublishedCache.HybridCache/Persistence/DatabaseCacheRepository.cs

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

* Update src/Umbraco.Infrastructure/Persistence/SqlSyntax/ISqlSyntaxProvider.cs

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

* fix Copilot comment

* fix wrong Copilot suggestion

* quote more column names

* resolve review

* Apply suggestions from code review

* synced interface and base class

* Revert "synced interface and base class". For an interface's default implementation, NotImplementedException makes more sense.

This reverts commit cf01cd01fc.

* Fixed remaining code warnings in DatabaseSchemaCreator.

---------

Co-authored-by: Andy Butland <abutland73@gmail.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-01-24 09:16:15 +00:00
83300221fe Log Viewer: Fix polling interval reset when changing intervals (closes #21507) (#21508)
* fix(log-viewer): prevent polling toggle reset when changing interval

Fixes issue where changing polling interval would reset the button to 'Polling' state instead of applying the new interval immediately.

- Remove togglePolling() call from closePoolingPopover() method
- Update setPollingInterval() to restart polling with new interval if already enabled

Fixes #21507

* refactor(log-viewer): extract polling start logic and fix regression

- Extract polling start logic into #startPolling() helper method
- Fix regression: enable and start polling when interval is selected while polling is off
- Update togglePolling() to use the helper method for consistency

Addresses feedback on PR #21508

---------

Co-authored-by: Gittensor Miner <miner@gittensor.io>
2026-01-23 17:17:02 +01:00
af81e258b4 Fix for the client side circular dependency. (#21464)
* Fix for the client side circular dependency.

This should fix the circular dependency without causing any breaking changes to the public APIs.

This issue is detailed here:
https://github.com/umbraco/Umbraco-CMS/issues/21463

* refactor UMB_MODAL_MANAGER_CONTEXT to avoid circular dependency

---------

Co-authored-by: Niels Lyngsø <nsl@umbraco.dk>
Co-authored-by: Niels Lyngsø <niels.lyngso@gmail.com>
2026-01-23 15:53:42 +00:00
aa4a33251a Content Types: Root properties (#21500)
implement root properties

Co-authored-by: Andreas Zerbst <73799582+andr317c@users.noreply.github.com>
2026-01-23 12:47:17 +00:00
Mads RasmussenandGitHub e7c63b19b9 Modal: Add 'in modal' condition to modal package (#21503)
* Add 'is modal' condition to modal package

Introduces a new 'is modal' condition for extension manifests, allowing actions to be conditionally permitted based on modal context. Updates user collection action manifests to use this condition, preventing certain actions when inside a modal. Includes implementation, configuration, manifest registration, and tests for the new condition.

* rename from is modal to in modal
2026-01-23 12:27:36 +00:00
Niels LyngsøandGitHub 16f9bc7cd7 Block List & Block Single: Use property value in validation check (Fixes #21313) (#21491)
use this.value as source for the validation
2026-01-23 12:12:20 +00:00
Niels LyngsøandGitHub 3b5e938492 Property Value Preset Builder: accept variant options (#21382)
property value preset builder to use variant options
2026-01-23 10:04:37 +00:00
ea76efafc1 Dictionary: Add configurable value search functionality (#21200)
* added dicationary value search active only with config param set

* Removed code smell, by reducing nesting

* Renamed configuration value to EnableValueSearch.
Added integration tests to verify search results.

* update query to return correct values for each language in the overview

* Use OptionsMonitor and add additional assert to verify fix to indication of which languages have translations.

---------

Co-authored-by: Andy Butland <abutland73@gmail.com>
2026-01-23 09:47:39 +01:00
1a68d39272 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:47:14 +01:00
Andy ButlandandGitHub fee3cc7352 Redirect Tracking: Handle empty string in redirect tracker when restoring from recycle bin (#21488)
Handle empty string in redirect tracker when restoring from recycle bin.
2026-01-23 14:25:51 +09:00
Nhu DinhandGitHub 3f3362e0f3 E2E: QA Updated tests for granular permission in content to match the changes (#21478) 2026-01-23 04:07:50 +00:00
Nicklas KramerandGitHub 8f55885829 Data Types: Allows transparency for the approved colors in the color picker. (#21495)
Fixing regex and adding test
2026-01-23 10:34:20 +09:00
8f6ebcdcf6 Picker: Support embedded Collections in the Collection Item Picker Modal (#21392)
* Add alias property to collection config interface

Introduced an 'alias' property to the UmbCollectionItemPickerModalCollectionConfig interface

* render collection element when modal is configured with an alias

* expose a picker modal route

* use collection in use picker

* adjust spacing

* add config option for selectOnly

* dynamic modal alias

* support selectable entity item ref

* wip entity data picker collection + ref and card views

* Add entity collection item card extension type + default elements

* implement user collection item card

* fix selection events

* map to prop

* add prop/attr for href

* add support for which detail properties to show

* update type import

* Update src/Umbraco.Web.UI.Client/src/packages/core/collection/item/entity-collection-item-card/entity-collection-item-card.element.ts

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

* import card in correct file

* Fix event listener binding for selection events

* implement disabled property for collection item cards

* init commit of collection item ref extension

* fix imports

* add element interface

* Implement UmbEntityCollectionItemElement interface in item cards

Added the UmbEntityCollectionItemElement interface to document and user collection item card elements for improved type safety and consistency. Updated type exports to include the new interface.

* Update collection item ref to use uui-ref-node

Replaces the placeholder div with a uui-ref-node component, passing relevant item properties and event handlers. Adds dynamic icon rendering using umb-icon.

* Refactor entity collection item elements to use shared base

Introduces a new abstract base class for entity collection item elements, consolidating shared logic for card and ref variants. Updates card and ref element implementations to extend the new base, and refactors extension manifest interfaces for consistency. This improves maintainability and reduces code duplication.

* use class instead of magic string

* Use entity collection item card in picker view

Replaces the placeholder card markup with the <umb-entity-collection-item-card> component, enabling selection and deselection functionality for items in the entity data picker card collection view.

* Update entity item ref to collection item ref

Replaces <umb-entity-item-ref> with <umb-entity-collection-item-ref> in the picker collection view. Adjusts event handlers and select-only logic to improve selection behavior and component consistency.

* utilise ref and card kind for picker views

* introduce ref and card collection view kinds

* Utilise card kind for user collection view

* Add item-specific href support to collection views

Introduces a requestItemHref method to collection contexts for retrieving item-specific hrefs. Updates card, ref, and user table collection views to use these hrefs, enabling dynamic linking for collection items. Refactors user table name column layout to accept href via value prop instead of constructing it internally.

* Update ManifestCollectionView import path

Changed the import of ManifestCollectionView from '../extensions/types.js' to '../view/types.js' to reflect its new location.

* remove unused

* use size medium for entity collection item picker

* use box

* render entity actions

* use edit path builder for user links

* rename method

* Revert "rename method"

This reverts commit 4df577688e.

* Update collection-default.context.ts

* make type lint ignore unused args with an underscore

* temp remove unused

* only make collection vie selectable if there are any registered bulk actions

* don't render name link if there is no href

* fix imports

* Render selection actions only if bulk actions exist

* use selectable state

* Update language-table-collection-view.element.ts

* Update language-table-collection-view.element.ts

* Update card-collection-view.element.ts

* clean up

* Refactor collection views to use shared base class

* refactor(collection): parallelize href fetching and make method private

* docs(examples): update collection example to use card and ref kinds

* docs(examples): add icon property to collection example data model

* Update src/Umbraco.Web.UI.Client/src/packages/core/collection/default/collection-default.context.ts

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

* Update src/Umbraco.Web.UI.Client/src/packages/core/collection/default/collection-default.context.ts

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

* Update src/Umbraco.Web.UI.Client/src/packages/core/collection/view/types.ts

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

* Update collection-bulk-action.manager.test.ts

* Removed duplicate and redundant '@typescript-eslint/no-unused-vars' rule definitions, consolidating the configuration to use only 'argsIgnorePattern'.

* Handle missing user href in name column layout

Replaces the user name link with a span when the href property is not provided, preventing broken links in the user table name column layout.

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

* pass modal data and value to routable modal

* Update picker-input.context.ts

* support selectableFilter

* scaffolding of a collection text filter extension

* Refactor collection text filter to use API interface

* Fix incorrect tag

* Update types.ts

* Update collection-text-filter.extension.ts

* Add cancelation to debounced search on destroy

* clean up

* add js docs

* two way binding of filter value

* clean up

* Add collection text filter manifest example

Introduced a new filter manifest for the example collection and updated the main manifests file to include it. This enables a text filter extension for the example collection.

* Delete unused element and context

* Update src/Umbraco.Web.UI.Client/src/packages/user/user-group/collection/user-group-collection.context-token.ts

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

* Update user-group-table-collection-view.element.ts

* support search for tree item and collection item pickers

* add spacing between collection ref items

* add margin between picker search result items

* remove spacing after last item

* remove padding in search results

* Update collection-item-picker-modal.element.ts

* move select only logic to collection selection manager

* add tests for collection selection manager

* change to filter label instead of search

* delete unused user grid collection view

* Select-only mode is now only disabled when all items are deselected, rather than on every deselection.

* prepare umb table for pickers

* utilize UmbCollectionViewElementBase in user table collection view

* remove console log

* handle select all and select item from same event

* bulk actions workaround

* add bulk action in collections feature toggle

* remove unused method

* make fields optional to avoid a breaking change

* remove unused import

* fix typescript errors

* adjust search styling

* hide with css

* fix ts errors

* Add modal data support to picker input context

Introduces methods to set and get modal data in UmbPickerInputContext, allowing base configuration for picker modals. Updates modal data handling to merge stored modal data with provided data for both direct picker opening and modal route setup.

* Fix bulk action manager test initialization

Added calls to setConfig in tests to properly initialize the observer before subscribing to hasBulkActions. Simplified the test logic for checking emissions when actions are present.

* Update tree-picker-modal.element.ts

* Update picker-search-result.element.ts

* Update src/Umbraco.Web.UI.Client/src/packages/core/collection/view/umb-collection-view-element-base.ts

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

* Use ifDefined for modal route in user input button

* Use ifDefined for href binding in entity data picker

* Fix collection alias binding in item picker modal

* wire up user table collection view with selectableFilter

* clean up controller aliases

* Update collection-item-picker-modal.element.ts

* Update collection-item-picker-modal.element.ts

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-01-22 15:24:09 +00:00
Andy ButlandandGitHub eca3e91af0 Management API: Fix document URLs returning all languages for invariant content (closes #21459) (#21473)
Fix display of all languages for URLs for invariant documents.
2026-01-22 11:41:01 +01:00
Niels LyngsøandGitHub 718e35f483 Media: Picker Modal types export (Fixes #21265) (#21329)
fix media modal exports
2026-01-22 10:03:18 +00:00
Niels LyngsøandGitHub ca8f6f59bd Entity Signs: Embed Api & Element for performance (#21480)
embed api & element
2026-01-22 09:18:31 +00:00
338f650274 Content-Type Designer: Transfer tab when moving property to inhertied tab (Fixes #20789) (#21234)
* enable async method

* ensure container is local to the owner content type

* no need to await anyhow

* handle moved groups

* Update src/Umbraco.Web.UI.Client/src/packages/content/content-type/workspace/views/design/content-type-design-editor-properties.element.ts

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

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-01-22 07:09:45 +01:00
Lee KelleherandGitHub 995e3bde6f Task: De-duplicate TypeScript class names (#21474)
De-duplicate TypeScript class names
2026-01-21 20:57:16 +01:00
7f162201e6 Fixes #20665 - Password change error msg (#21257)
* Fixes #20665 - Password change error msg

In order to show the right validation message:
 - the repository code always notifies the validation failure message
    (or a default failure message if none is received)
 - in the data-source code, tryExecute is called with the option
    to disable the default notification

* Return the original error instead of faking success

---------

Co-authored-by: Emma L Garland <1649855+emmagarland@users.noreply.github.com>
2026-01-21 13:48:25 +00:00
d7dbe39dd3 Routing: Add DocumentUrlAliasService for optimized URL alias lookups (closes #21383) (#21396)
* Implement document alias cache and service to optimize content finder by alias.

* Renamed to DocumentUrlAlias. Fixed issues on start-up.

* Remove tracking of root ancestor.

* Optimize cache key, tidy up tests, move domain matching to content finder.

* Handle language and document deletes.

* Align further with document URL service.

* Code tidy.

* Fixed comment.

* Refactor scope handling to avoid nested scopes

Extract CreateOrUpdateAliasesInternalAsync to process documents without
creating their own scope. Both CreateOrUpdateAliasesAsync and
CreateOrUpdateAliasesWithDescendantsAsync now create a single scope
and call the internal method, avoiding unnecessary nested scope creation.

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

* Extract CreateOrUpdateAliasesInternalAsync to process documents without
creating their own scope.

* Only return a document for a match under a domain if the document is found under the domain of the current request.

* Fix failing integration tests.

* Apply suggestions from code review.

* Ensured language to culture code map is updated when a language isn't found in the cached map.

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-21 13:52:45 +01:00
b2801e6555 Backoffice: Fix event listener memory leaks in auth, dropzone, actions, and router (#21458)
fix(backoffice): resolve event listener memory leaks in auth, dropzone, actions, and router

Fixes memory leaks in 4 components where event listeners registered with .bind(this) could not be properly removed because each .bind() call creates a new function reference.

Changes:
- auth.context.ts: Convert #onStorageEvent to arrow function property
- dropzone-media.element.ts: Convert 4 drag handlers to arrow function properties
- entity-actions-dropdown.element.ts: Convert handler and add disconnectedCallback
- router-slot.element.ts: Convert handler and add proper cleanup in disconnectedCallback

Solution: Arrow function properties maintain consistent references while preserving 'this' context, enabling proper listener removal.

Testing:
- Added unit tests for auth.context.ts
- All builds pass
- Linter passes
- No breaking changes

Documentation:
- Added "Event Listener Cleanup Pattern" section to clean-code.md
- Added "Event Handler Guidelines" section to style-guide.md

Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-21 11:12:09 +00:00
594b64e2a9 Tiptap RTE: Optimize umb-input-tiptap initialization and rendering (#21070)
* refactor(rte): Replace misleading Promise.all with sequential awaits

The inner awaits in Promise.all([await ..., await ...]) made the operations
sequential anyway. Since #loadEditor() depends on _extensions being populated,
sequential execution is correct - this change makes the intent clearer.

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

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

* perf(rte): Cache toolbar and statusbar emptiness checks

Instead of calling .flat() on every render to check if toolbar/statusbar
have items, compute the boolean once when values are set in #loadEditor().
This avoids unnecessary array operations during render cycles.

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

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

* perf(rte): Pre-compute extension styles during initialization

Instead of calling unsafeCSS() on each style during every render cycle,
collect and process styles once in #loadEditor() and store the result
in _extensionStyles. This avoids repeated CSS processing during renders.

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

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

# Conflicts:
#	src/Umbraco.Web.UI.Client/src/packages/tiptap/components/input-tiptap/input-tiptap.element.ts

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-21 10:19:21 +01:00
acd0aae48e DevOps: Adds check:duplicate-class-names devops script (#21460)
* Adds `check:duplicate-class-names` devops script

* DevOps: Improve `check:duplicate-class-names` script

- Fix example path in JSDoc comment
- Add support for `export default class` declarations
- Add `--ignore-stories` flag to exclude story files from detection

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

* Added try/catch on reading file contents

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Co-authored-by: Andy Butland <abutland73@gmail.com>
2026-01-21 10:18:52 +01:00
Jacob OvergaardandGitHub d4c3813410 Dotnet Template: Removes unused setting SanitizeTinyMce (#21467)
chore: removes unused setting `SanitizeTinyMce`
2026-01-21 08:53:59 +00:00
Nikolaj GeisleandGitHub 5d664538f0 Examine: Check for registered populators before emptying indexes (#21455)
* Check for registered populators before emptying indexes

* Update can rebuild to also use HasRegisteredPopulator
2026-01-21 08:33:03 +01:00
Andy ButlandandGitHub c8f897879c Performance: Fix thread safety and optimize cache updates in PublishStatusService after content changes (#21415)
* Resolved potential thread safety issues with PublishStatusService.

* Only update published status in content cache refresher if within a publish or unpublish operation.
2026-01-21 06:41:55 +00:00
cee47613a6 Move media-type guid strings into constants partial (#21461)
* move media-type guid strings into constants partial

* missed one.

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

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

* Add member type GUID constants too.

* Removed member type incorrectly recorded as a built-in data type.

* Reuse constant in obsolete GUID constant.

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Andy Butland <abutland73@gmail.com>
2026-01-21 06:25:45 +00:00
ac1a0a46df Document URL Cache: Ensure URLs are rebuilt after upgrade and prevent duplicate initialization (closes #21337) (#21379)
* Remove rebuild of document URLs during migration, instead ensuring they will run after migration is complete and Umbraco is running.

* Avoid unnecessary second rebuild of document URL cache after startup with migration that has already triggered a rebuild.

* Apply suggestions from code review

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

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-01-21 06:54:56 +01:00
8f571fe51b Rollback: Add toggle for diff display (closes #18518) (#21426)
* Add a toggle, defaulted to off, for display of diffs on the rollback view.

* Used only label for checkbox.

* Align formatting across translations for diffHelp key.

* Changed the checkbox to a toggle

UI semantics, checkboxes imply selection, whereas toggles imply activation.

---------

Co-authored-by: leekelleher <leekelleher@gmail.com>
2026-01-20 15:44:04 +00:00
Niels LyngsøandGitHub a3b4e922d9 Performance: Use import maps to save requests (#21363)
Use import maps to save requests
2026-01-20 14:27:11 +00:00
fd7a2c6a5a Content picker: Prevent selection of document/member type containers when configuring allowed types (closes #21356) (#21357)
* Prevent selection of document and member type folders when selecting allowed types for the content picker.

* Added fix for Media Types

* Set `documentTypesOnly` on `umb-input-document-type`

so to disallow selecting element-types.

* Linting

---------

Co-authored-by: leekelleher <leekelleher@gmail.com>
2026-01-20 12:50:37 +00:00
fd09a24559 Media: Unable to see the "Access denied" view when deep-linking to restricted media nodes (#21442)
* fix: aligns media workspace with document workspace to handle "variants" when calculating routes, which fixes an issue where the "Access denied" view would not be shown

* fix: clear root access flag when selecting specific start nodes

When selecting specific document or media start nodes for a user, the UI now automatically sets hasDocumentRootAccess/hasMediaRootAccess to false.

Previously, if a user group had "Has access to all items" enabled, selecting specific start nodes on the individual user wouldn't clear the root access flag. This caused the backend to add -1 (root access) to the start node list, overriding the specific node selections.

This ensures user-specific start node permissions properly override group-level root access settings.

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

* fix: add length check to prevent rendering router with empty routes array

The render method now checks both that _routes exists AND has length > 0 before rendering the router-slot. An empty array is truthy, so without the length check, the router-slot could be rendered with an empty routes array, causing runtime errors.

This aligns with the original render logic and prevents the TypeError when media tests run.

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

* Fix E2E test URL construction for media workspace deep-linking

The test was constructing an invalid URL by appending the workspace path
directly to the current URL, which included '/collection'. This resulted in:
/umbraco/section/media/collection/workspace/media/edit/ (invalid)

Instead of the correct:
/umbraco/section/media/workspace/media/edit/

The fix removes '/collection' before appending the workspace path, ensuring
the test actually navigates to the workspace editor where the 'Access denied'
view is properly displayed.

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

* Make all tests for media start node run in the pipeline - remember to revert before merging

* Revert npm command before merging

---------

Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
Co-authored-by: Nhu Dinh <hnd@umbraco.dk>
2026-01-20 10:42:12 +00:00
2eb2e33f2f Document Tree: Filter tree items based on user browse permissions (closes #21141) (#21173)
* Document Tree: Filter tree items based on user browse permissions

- Add FilterTreeEntities virtual methods to EntityTreeControllerBase for filtering tree entities with total count adjustments
- Override FilterTreeEntities in DocumentTreeControllerBase to filter by ActionBrowse permission
- Extract filtering logic into IDocumentPermissionFilterService for testability
- Add unit tests for DocumentPermissionFilterService

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

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

* Apply suggestions from code review

Co-authored-by: Copilot <175728472+Copilot@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-01-20 10:57:13 +01:00
ddffd6ec1e HybridCache: Optimize content type change cache rebuild to resolve SQL timeouts (#21207)
* Complete the scope when no runnable job found. Without this I'm seeing timeouts and lock contention if a long-running document type save operation is running when the first distributed job is requested.

* Run serialization steps of rebuild of content cache in parallel for a small but not insignficant speed optimization.

* Add integration tests for database cache rebuild.

* Optimize rebuild of databaes and memory cache after content type update.

* Add debug log for running distributed job.

* Apply memory cache clear optimization to media.

* Optimize MediaCacheService.RebuildMemoryCacheByContentTypeAsync with lightweight query

Use GetMediaKeysByContentTypeKeys to fetch only media keys instead of loading full ContentCacheNode objects. This matches the same optimization applied to DocumentCacheService.

Also refactors Rebuild() to reuse RebuildMemoryCacheByContentTypeAsync for the memory cache clearing step.

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

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

* Apply suggestions from code review

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

* Further updates from code review.

* Further tests for variant documents, composed documents and message pack serialization.

* Fixed failing integration tests.

* Clear the cacje level published content cache on content type change.

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-01-20 09:15:40 +01:00
e206f26e5e Content Picker: Provide "content root" origin for dynamic root (closes #21134) (#21161)
* Provide "content root" origin for dynamic root.

* Update src/Umbraco.Core/DynamicRoot/Origin/ContentRootDynamicRootOriginFinder.cs

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

* Code tidy.

* Add integration test

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Sven Geusens <geusens@gmail.com>
Co-authored-by: Sven Geusens <sge@umbraco.dk>
2026-01-20 06:56:23 +01:00
Chris HoustonandGitHub cbaab6a6f3 Code Quality: Adding XML documentation to Umbraco.Cms.Persistence.EFCore.SQLServer & Umbraco.Cms.Persistence.EFCore.SQLite (#21439)
Adding XML documentation to these two projects.

- Umbraco.Cms.Persistence.EFCore.SQLServer
- Umbraco.Cms.Persistence.EFCore.SQLite
2026-01-20 06:50:24 +01:00
Chris HoustonandGitHub 4f62771135 Code Quality: Resolve 128 SA1600 documentation warnings in Umbraco.Cms.Persistence.Sqlite (#21438)
* fix: Resolve 128 SA1600 documentation warnings in Umbraco.Cms.Persistence.Sqlite

- Added XML documentation comments to interceptors, mappers, and services
- Added TODO (V18) comments to SqliteSyntaxProvider.Format methods (CS0114)
- Updated .csproj TODO comment to follow V18 convention
- CS0114 warnings remain suppressed as fix would be binary breaking

* Fixed the issues Copilot complained about with the documentation and..

Fixed two IDE0270 warnings (null check simplification).
2026-01-20 06:48:32 +01:00
Chris HoustonandGitHub a4e72cbdb1 Code Quality: Adding all missing XML documentation for the Umbraco.Cms.Persistence.EFCore project (#21437)
Adding all missing XML documentation for the Persistence.EFCore project
2026-01-20 06:42:00 +01:00
Chris HoustonandGitHub 8d75277297 Code Quality: Fix CS0659 and CS0661 build warnings in Item test class by removing legacy test and setup of little value (#21399)
* Code Quality: Fix CS0659 and CS0661 build warnings in Item test class

The Item class in test project defined Equals override and equality operators without implementing GetHashCode, causing CS0659 and CS0661 compiler warnings.

Added GetHashCode implementation using RuntimeHelpers.GetHashCode(this) for consistent reference-based equality matching the existing operators behavior.

Removed CS0659/CS0661 from WarningsNotAsErrors in test project as they are no longer needed.

* Code Quality: Remove unused test infrastructure classes

Remove Item, OrderItem, and SimpleOrder classes along with the SimpleOrder_Returns_Null_On_FirstOrDefault_When_Empty test.

These ~370 lines of test infrastructure existed only for a single trivial test that verified FirstOrDefault() returns null on an empty collection - behavior already tested on actual Umbraco collections in the same file.
2026-01-20 06:32:51 +01:00
Andy ButlandandGitHub d4fe1b3783 StringExtensions: Refactor into partial classes and optimize methods (#21370)
* Refactor StringExtensions into multiple files using partial classes.

* Tidy/complete XML header comments.

* Fixed warnings in string extension methods.

* Add unit tests for IsLowerCase and IsUpperCase and optimize the methods.

* Add unit tests for ReplaceNonAlphanumericChars and optimize the method.

* Add unit tests for StringWhitespace and optimize the method.

* Add unit tests for StripHtml and DecodeFromHex and optimize the methods.
Fix too aggressive regex for StripHTML to ensure works only on HTML tags.

* Add unit tests for EnsureStartsWith and EnsureENdsWith and optimize the methods.

* Add unit tests for ToSingleLine and StripNewLines and optimize the methods.

* Fix issues raised in code review.
2026-01-19 19:18:39 +01:00
Andreas ZerbstandGitHub e767914bd0 E2E: QA Added separate emails for the login tests (#21443) 2026-01-19 14:33:56 +00:00
7acdc6ec0b Router: Destroy route component when disconnected (Fixes #21272) (#21318)
destroy route component when disconnected

Co-authored-by: Mads Rasmussen <madsr@hey.com>
2026-01-19 11:11:26 +01:00
Nhu DinhandGitHub 00971bb55e E2E: QA Fixed failing tests for setting up content notifications (#21441) 2026-01-19 08:39:27 +00:00
Chris HoustonandGitHub 9832603525 Code Quality: Resolve SA1649 warnings (#21401)
* Added the SA1649 to the "No Warnings" section.

Stylecop is trying to enforce filenames that are like:

CancellableObjectEventArgs{TEventObject}.cs

However Umbraco uses CancellableObjectEventArgs.cs

Unless a policy decision is make to follow this stylecop rule, I think it is better to add this rule to the " NoWarn " section, so we don't see it appear at all.

* Revert accidental package-lock.json change

* Renaming files to match the StyleCop patterns.

Except two which would end up having the same names as other file, so these have been renamed as LegacyIScope & LegacyIScopeProvider, with local Pragma warnings disabled for this Style Cop rule.

* Removing the SA1649 from Warnings NOT as Errors.

In other words, if you turn on show warnings as errors, these will show as errors, rather than being suppressed.
2026-01-17 14:35:01 +01:00
Jacob OvergaardandGitHub e485056b8d Bulk Publish: Filter variant options to applicable cultures only (closes #19147) (#21163) 2026-01-16 19:25:22 +01:00
Andy ButlandandGitHub 28a3884adb Revert binary breaking changes from PR #21236 (#21434)
* Revert binary breaking changes from #21236 and comment with a TODO for the next major.

* Further TODO.
2026-01-16 17:46:59 +01:00
Mads RasmussenandGitHub 0eaca92615 Backoffice Performance: Inline entry point modules to reduce JS chunk count (#21380)
* change to static import

* add support for passing modules to manifest js property

* Replaces dynamic imports of entry-point.js with static imports across all manifests

* Support statically imported modules in loader functions

Extended loadManifestApi and loadManifestElement to handle already resolved module objects (statically imported modules) in addition to dynamic imports. Updated type definitions in utils.ts to include module export types for loader properties.

* Add tests for loadManifest* functions in extension-api

Introduces unit tests for loadManifestApi, loadManifestElement, and loadManifestPlainJs functions. These tests cover various scenarios including direct class constructors, dynamic and static imports, export prioritization, and edge cases for null and undefined inputs.
2026-01-16 15:18:59 +00:00
Engiber LozadaandGitHub 9ed3186cc2 Content Workspace: Add condition to detect when a content workspace has finished loading. (#21290)
* Added folder and files for the new condition.

* Registered the condition.

* Added an example to test the condition.

* Added the condition in one of examples.

* Renamed condition.

* Fixed linting error.
2026-01-16 15:59:01 +01:00
10c5df892f Notification Container: Make toast notifications announced by screen readers in Chrome. (#21028)
* fix(a11y): Toast notifications not announced by screen readers in Chrome

- Move screen reader live region from Shadow DOM to Light DOM (document.body)
  Chrome doesn't reliably detect ARIA live regions inside Shadow DOM
- Use role="alert" with fresh elements for each announcement instead of
  updating text content of an existing live region
- Fix invalid aria-role="true" attribute (was invalid HTML)
- Fix missing backslash in unicode escape '\u00A0'

The previous implementation had the live region nested 3 levels deep in
Shadow DOM, which Safari handled but Chrome ignored. Creating a new
alert element in Light DOM for each announcement is the most reliable
method across browsers.

Closes #14521

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

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

* Removed comment.

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-16 15:57:46 +01:00
Niels Lyngsø 90be5d9958 lint fixes 2026-01-16 14:51:20 +01:00
Bjarne FyrstenborgandGitHub 4d5f9ec7ce Focal point: Utility functions (#21264)
Focal point utils
2026-01-16 13:20:19 +00:00
Andy ButlandandGitHub 07e25d681b Media Picker: Respect start node when drag+dropping files directly onto picker (closes #21422) (#21423)
Pass parent unique to the media to the media picker dropzone.
2026-01-16 11:51:45 +01:00
CopilotGitHubcopilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>iOvergaardEngiber Lozada
ba7f84c96f Thumbnails: Fix image thumbnails cropping to allow the entire image to be shown as a thumbnail (closes #20347) (#21288)
* Initial plan

* Change object-fit from cover to contain for image thumbnails

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

* Add visual demonstration of the fix

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

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: iOvergaard <752371+iOvergaard@users.noreply.github.com>
Co-authored-by: Engiber Lozada <89547469+engijlr@users.noreply.github.com>
2026-01-16 11:50:26 +01:00
Dirk SeefeldandGitHub f36947a947 Replace nameof() by constants of DTO (closes #21303) (#21344) 2026-01-16 10:59:03 +01:00
JeavonandGitHub f6230cd122 Update Umbraco and Starter Kit versions in templates (#21395)
* Update Umbraco version in starterkits template

LTS and Latest should both install 17.0.0, at the moment latest uses Umbraco v17.1.0 but a starter kit version 17.0.0-rc1 which is not a good combo

* Update LTS in template to 17.1.0
2026-01-16 10:04:48 +01:00
a2c743ca43 Persistence Model: Replace some hard coded strings in DTOs (#21327)
* Squash merge Squash merged v173/20453-fix-more-sql-syntax-issues int v173/20453-fix-more-sql-syntax-issues-squash (copy of main)

* fix 2 unit test

* fix Copilot review comments

* resolve review comments

* replace more hard coded strings

* fix test

* fix review comments

* fix database schema

* fix database schema and ResultColumn reference names

* Update src/Umbraco.Infrastructure/Persistence/Dtos/ContentTypeAllowedContentTypeDto.cs

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

* add comment  from review

* fix two reference column names

* fix breaking change

* fix typo

* Remove unnecessary attributes

* mark 2 unsused DTO classes as obsolete

* reverted change of class UnionHelperDto adding [Column("...")] attributes again, because some integration tests for PostgreSQL provider fail without them. Again a case sensitivty issue.

* replace nameof reference names,
make all column name const consistent

* Update obsoletion messages

---------

Co-authored-by: Andy Butland <abutland73@gmail.com>
2026-01-16 06:18:53 +00:00
Niels Lyngsø d72d66edfd no delay on forbidden-text animation 2026-01-15 16:35:00 +01:00
9c50476b17 Tree Navigation: Add visual indicators for items with restricted access (#21365)
* Tree pickers: Implement noAccess property UI handling for user start nodes

- Add noAccess observable to document and media tree item contexts
- Add visual styling (grayed out, italic) for noAccess items in tree views
- Update document and media picker input contexts to prevent selection of noAccess items
- Items with noAccess are shown for navigation but cannot be selected in pickers

This implements the UI handling for Feature 63060 "Handle Start Nodes"

* Apply suggestions from code review

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

* Fix noAccess implementation and add E2E tests

This commit combines all improvements made to the noAccess property feature:

1. Refactored to use Lit lifecycle methods (updated()) instead of property watchers
2. Added click and keyboard event handlers to prevent navigation
3. Removed disabled attribute that was blocking tree expansion
4. Added comprehensive E2E tests for document and media trees

Critical bug fix: Removed disabled attribute that prevented expansion
- The disabled attribute was blocking ALL interactions including expanding
  tree items to show accessible children underneath noAccess ancestors
- Now only sets aria-disabled="true" for screen readers and removes href
- Click and keyboard event handlers still prevent navigation as intended
- Users can now properly navigate through noAccess ancestors to reach
  their accessible child nodes

E2E test coverage:
- Display noAccess styling (opacity, italic)
- Prevent navigation when clicking noAccess nodes
- Allow expansion of noAccess nodes to show children
- Picker tests skipped pending infrastructure improvements

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

* Remove aria-disabled manipulation that interferes with tree expansion

The previous implementation set aria-disabled="true" and removed href
from the menu-item in #updateMenuItemAccessibility(). This approach
caused issues with tree expansion functionality.

Removed:
- #updateMenuItemAccessibility() method
- updated() lifecycle hook that called it
- UUIMenuItemElement import (no longer needed)

The click and keyboard event handlers already prevent navigation to
noAccess nodes, so additional DOM manipulation is not necessary.

Test results:
 4 passing: Display styling and prevent navigation work correctly
 2 failing: These appear to be backend issues:
   1. Document expansion: Caret button disabled (backend marking noAccess
      items as not selectable, which disables entire menu-item)
   2. Media expansion: Child media folder incorrectly has noAccess attribute
      (backend data issue - child should be accessible as it's the start node)

The UI implementation is sound. The remaining test failures indicate
backend API issues that need investigation.

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

* Fix path comparison bug in UserStartNodeEntitiesService (similar to #21162)

This fixes the same path comparison bug we fixed in PR #21162 but in C# string
comparisons instead of SQL queries.

## Root Cause
Path comparisons without trailing commas caused false matches:
- Path "-1,1001" incorrectly matched prefix "-1,100"
- This marked nodes as ancestors/descendants when they weren't related

## Examples of False Matches
- child.Path = "-1,1001", startNodePath = "-1,100"
  - OLD: "-1,1001".StartsWith("-1,100") = TRUE (bug!)
  - NEW: "-1,1001,".StartsWith("-1,100,") = FALSE (correct!)

- child.Path = "-1,100", startNodePath = "-1,1001"
  - OLD: "-1,1001".StartsWith("-1,100") = TRUE (bug!)
  - NEW: "-1,1001,".StartsWith("-1,100,") = FALSE (correct!)

## Fix Applied (Two Locations)
1. Line 146 (ancestor check): Added comma suffix to child.Path
2. Line 226 (IsDescendantOrSelf): Added comma suffix to both paths

This matches the pattern already used correctly in lines 92 and 191 of the
same file, and mirrors the SQL fix from PR #21162.

## Test Impact
This should fix the failing E2E test where child media folders were incorrectly
marked as noAccess when they were actually the user's start node.

Related: #21162

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

* Fix remaining merge conflict markers in media-tree-item.element.ts

* Remove E2E agent markdown file (moved to personal space)

* test: adds mock data for noAccess

* feat: moves noAccess subscriber to base class

* test: adds mock data for media

* feat: moves no-access styling to the base class

* fix: media tree items should inherit styling from the base class

* feat: observes noAccess from children and reports back to the base class

* test: spec file should use undefined instead of null

* docs: add comprehensive comments explaining noAccess opt-in pattern

- Document why noAccess is not in base interface (breaking change)
- Explain opt-in pattern with code examples
- Add JSDoc comments to property, event handlers, and CSS
- Reference accessibility considerations (keyboard users)
- Link child class implementations to base class documentation

* test: adds timeout for URL to settle

* fix: allow clicks on accessible children of noAccess tree items

When a tree item has noAccess, child tree items are rendered in its slot.
Previously, the parent's click handler blocked ALL clicks due to event bubbling,
preventing users from navigating to accessible descendants.

Now checks if click originated from a child tree item element using closest().
If it's a child, allow the click. Only block clicks on the noAccess item itself.

Applied to both mouse clicks and keyboard navigation (Enter/Space).

This enables users to navigate through noAccess ancestors to reach their
accessible start nodes (e.g., Root[noAccess] → Child[noAccess] → Grandchild[accessible]).

Fixes tests:
- should allow expansion of noAccess ancestor node to show children (documents)
- should allow expansion of noAccess ancestor media node to show children (media)

* compare with the closest element to see if we are clicking on the element that is blocked or a sub-element that is not

* fix: adds forbidden route in case of no variants

* test: corrects label locator

* test: adds test to check if you can click or deeplink to restricted media

* test: removes .only

* test: removes duplicated tests

* test: adds test for document no-access

* test: add unit tests for user start node path comparison logic

Adds comprehensive unit tests documenting the path comparison fix that prevents
false matches when node IDs are numeric prefixes of other IDs (e.g., 100 vs 1001).

The fix uses trailing commas on both paths to ensure accurate comparison:
- Without fix: "-1,100".StartsWith("-1,10") = true  (incorrect)
- With fix: "-1,100,".StartsWith("-1,10,") = false  (correct)

Tests cover:
- Numeric prefix edge cases (1 vs 10, 10 vs 100, 100 vs 1001)
- Self comparison (start node itself)
- Descendant relationships
- Deep path hierarchies
- Demonstrates the bug without the fix for documentation

19 test cases total, all passing.

* test: removes .only

* fix: do not overwrite forbidden route

* docs: fixes line number in comment

* test: fixes comment

* feat: uses isSelectableContext to disable and scrub 'href' from base element

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-01-15 16:29:21 +01:00
7dca122a80 NPM: Move Umbraco Package Schema and custom-elements to root level for IDE discoverability (closes #16667) (#17866)
* Adjust build scripts for custom elements and JSON schema generation to be placed at root level, add generation to build for npm and update .gitignore

* fix: updates umbraco package schema location

* git ignores

* fix: outputs the vscode custom elements file at root

* fix: adds generated files to output

---------

Co-authored-by: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com>
2026-01-15 15:11:26 +00:00
e7de6a8afb Backoffice: Fix login logo popover to display Umbraco branding (closes #21078) (#21413)
* fix(backoffice): use hardcoded Umbraco logo in header popover

Fixes issue where the backoffice header logo popover incorrectly
displayed the LoginLogoImageAlternative setting instead of showing
the Umbraco branding.

Changes:
- Added hardcoded umbraco-logo.svg asset to client project
- Updated backoffice-header-logo component to reference static logo
- Wrapped logo in link to umbraco.com
- Removed dependency on BackOfficeLogo endpoint for popover

The small header logo button still uses <umb-app-logo> and remains
customizable via the BackOfficeLogo setting.

Closes #62866

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

* chore: removes link to umbraco.com

---------

Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-15 15:27:12 +01:00
d6de4a611a Media Picker: Uploaded files should automatically be selected (closes #21115) (#21409)
* fix(media-picker): auto-select uploaded media items

When uploading media in the media picker modal, uploaded items are now
automatically selected. This works for both single and multiple selection
modes, and correctly handles paginated folders where uploaded items may
not be visible on the current page.

Closes #21115

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

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

* fix(media-picker): navigate to last page after upload

Uploaded media items get the highest SortOrder, placing them on the last
page. This change navigates to the last page after upload so users can
see their newly uploaded items, which are also auto-selected.

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

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

* Update src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/media-picker-modal.element.ts

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

* Apply suggestion from @Copilot

Co-authored-by: Copilot <175728472+Copilot@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-01-15 13:06:00 +01:00
Nhu DinhandGitHub 65d7989c23 E2E: QA Add acceptance tests for user group description (#21404)
* Added tests to create a user group with description

* Clean up

* Moved tests for user group description to other class

* Bumped version

* Make tests run in the pipeline

* Reverted npm command
2026-01-15 10:54:37 +00:00
226162d8f2 Performance: Optimize refresh of hybrid cache for a document by retrieving draft and published in single query (#21407)
* Optimize retrieval of ContentCacheNode for draft and publish in when refreshing the hybrid cache.

* Fixed issue with XML header documentation tags.

* Use is null for consistency

---------

Co-authored-by: mole <nikolajlauridsen@protonmail.ch>
2026-01-15 10:52:21 +00:00
dependabot[bot]andJacob Overgaard b484403b1c Bump the npm_and_yarn group across 2 directories with 1 update
Bumps the npm_and_yarn group with 1 update in the /src/Umbraco.Web.UI.Client directory: [diff](https://github.com/kpdecker/jsdiff).
Bumps the npm_and_yarn group with 1 update in the /src/Umbraco.Web.UI.Client/src/packages/core directory: [diff](https://github.com/kpdecker/jsdiff).


Updates `diff` from 7.0.0 to 8.0.3
- [Changelog](https://github.com/kpdecker/jsdiff/blob/master/release-notes.md)
- [Commits](https://github.com/kpdecker/jsdiff/compare/7.0.0...v8.0.3)

Updates `diff` from 7.0.0 to 8.0.3
- [Changelog](https://github.com/kpdecker/jsdiff/blob/master/release-notes.md)
- [Commits](https://github.com/kpdecker/jsdiff/compare/7.0.0...v8.0.3)

---
updated-dependencies:
- dependency-name: diff
  dependency-version: 8.0.3
  dependency-type: direct:production
  dependency-group: npm_and_yarn
- dependency-name: diff
  dependency-version: 8.0.3
  dependency-type: direct:production
  dependency-group: npm_and_yarn
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-01-15 11:22:54 +01:00
f000b00c65 Server Events: Add runtime state check and error handling to ServerEventRouter (#21406)
Add resilience to ServerEventRouter to prevent failures during unattended
install/upgrade when SignalR (especially Azure SignalR) is configured.

Changes:
- Skip server event routing when runtime level is not Run (Install/Upgrade)
- Add try-catch with warning logging for graceful degradation on SignalR failures
- Add backwards-compatible obsolete constructor using StaticServiceProvider pattern
- Add unit tests for runtime level checks

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-15 10:20:06 +00:00
Chris HoustonandGitHub 720774d219 Code Quality: Fix CS1574 and CS0419 XML documentation warnings (#21400)
Docs: Fix CS1574 and CS0419 XML documentation warnings

Fixed 17 build warnings related to XML documentation cref attributes:

CS1574 (cref attribute could not be resolved):
- IApiMediaQueryService: Changed see cref to paramref for path parameter
- Permission resources: Removed cross-assembly cref to handlers in Management API
- ContentService: Removed reference to non-existent SaveAndPublish method
- ModelsBuilderModeValidator: Fixed cref to Constants.ModelsBuilder.ModelsModes.Nothing
- ImageCropperPropertyValueEditor: Fixed cref to TemporaryFileUploadValueBase.Src
- NPocoSqlServerDatabaseExtensions: Removed cref to external NPoco method
- RegisteredReloadableLogger: Removed cref to non-existent RefreshingRazorViewEngine
- FriendlyPublishedContentExtensions: Removed cref to non-existent AncestorOrSelf methods
- ManagementApiControllerBase: Removed cref to inherited Forbid() method
- BackOfficeExternalLoginProviderErrorMiddleware: Fixed namespace in cref
- HasScheduleFlagProvider: Fixed typo HasScheduleSignProvider -> HasScheduleFlagProvider

CS0419 (ambiguous cref reference):
- ServiceCollectionExtensions: Specified exact overload ConfigureUmbracoDefaults(IHostBuilder)
- IUserStartNodeEntitiesService: Replaced ambiguous GetPagedChildren cref with plain text
2026-01-15 06:45:13 +01:00
Chris HoustonandGitHub e389e3c505 Removing a variable that is not being used - fixes warning CS0168 (#21398) 2026-01-15 06:30:52 +01:00
9bf54ca9cb UI: Refactor breadcrumb URLs to use Path Constants (#21179)
- Add UMB_WORKSPACE_EDIT_PATH_PATTERN and UMB_WORKSPACE_EDIT_VARIANT_PATH_PATTERN
  to core workspace paths for generic edit URL generation
- Fix UmbPathPattern to support multi-level chaining via toAbsolutePatternString()
- Refactor workspace-menu-breadcrumb to use new path patterns
- Refactor menu-variant-tree-structure-workspace-context-base to use new patterns
- Refactor tree-item-context-base to use UMB_WORKSPACE_EDIT_PATH_PATTERN
- Refactor user-grid-collection-view to use existing UMB_EDIT_USER_WORKSPACE_PATH_PATTERN
- Remove outdated TODO about encoding uniques (handled at data source)

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

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Co-authored-by: Mads Rasmussen <madsr@hey.com>
2026-01-14 11:08:49 +01:00
59675a083d Code Quality: Fix StyleCop warnings SA1116, SA1401, SA1649, SA1405, SA1121, SA1130, SA1306, SA1028, SA1400, SA1106 (#21377)
* fix(stylecop): resolve SA1106 - remove empty statement

* fix(stylecop): resolve SA1400 - add missing access modifiers

* fix(stylecop): resolve SA1028 - remove trailing whitespace

* fix(stylecop): resolve SA1306 - rename fields to lowercase

* fix(stylecop): resolve SA1130 - use lambda syntax

* fix(stylecop): resolve SA1121 - use built-in type aliases

* fix(stylecop): resolve SA1405 - add messages to Debug.Assert calls

* fix(stylecop): resolve SA1649 - rename files to match type names (partial)

* fix(stylecop): resolve SA1401 - convert fields to const/readonly (partial)

* fix(stylecop): resolve SA1116 - reformat multi-line parameters (partial)

* fix(stylecop): revert breaking changes, add V18 TODO comments

* fix: correct TODO comment for SA1306 - should rename to _completed

* Standardize API file names across modules - No code changes, file names.

- Extracts login model to a dedicated file and preserves binding behavior
- Renames multiple API files to align with updated conventions ( Just to match their names in the code, not changing the actual API names, i.e. no breaking changes )
- Updates DI extensions, mappings, and OpenAPI helpers to follow new naming
- Adjusts tests for consistent formatting and readability
- Preserves behavior; no logic changes, references kept intact

* fix(tests): refactor UserEmail to virtual property pattern

- Convert protected field _userEmail to virtual property UserEmail
- Remove dead code (_userEmail += "groupName" executed after request)
- Update derived test classes to use property instead of field
- Maintains original name to avoid breaking changes
- Follows best practice: virtual property allows derived class override

This was originally changed in my PR from UserEmail to _userEmail, so changing it back to ensure no breaking change, even though this is in a test class.

* Committing small fix to prevent a breaking change, adding commit for future removal.

* Renames helper class and removes BOM

Renames internal helper to follow naming conventions without the T prefix
Removes stray BOM from header to ensure clean compilation
No runtime behavior changes

* Split Physical FileSystem interface into it's own file.

* Split the IContentQueryService into it's own file

Also updated XML docs.

* Reverting the package-lock.json

* Updated the typo for Permision -> Permission

Updated the file name and class to: AddUserGroup2PermissionTable

This should be safe to do so as migrations are logged with their GUID's not the class names.

* Update src/Umbraco.Core/Scoping/CoreScope.cs

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

* Reverting a binary change.

* Reverted rename of public migration class.

* Revert name in migration plan.

---------

Co-authored-by: Andy Butland <abutland73@gmail.com>
2026-01-14 09:17:47 +00:00
Sebastiaan JanssenandGitHub a4a6c37c8b Only run this scheduled job on the original repo, not on forks 2026-01-14 10:01:23 +01:00
Andreas ZerbstandGitHub 396a8edd91 E2E: QA updated test helpers to fix flaky acceptance tests (#21373)
* Bumped helpers

* Updated tests

* Bumped version

* Bumped version again to fix flaky renaming test
2026-01-14 08:58:05 +01:00
Andy ButlandandGitHub de87a71bc8 Migrations: Ensure description column is added before earlier User Group migration runs (#21378)
* Ensure the description field added in a later migration for user groups is available when the earlier migration on this table runs.

* Update implementation of fix to store and use the state of UserGroupDto at the time of migrations.
2026-01-14 07:59:39 +01:00
abe772b963 Collection: Introduce Collection Text Filter Extension (#21172)
* scaffolding of a collection text filter extension

* Refactor collection text filter to use API interface

* Fix incorrect tag

* Update types.ts

* Update collection-text-filter.extension.ts

* Add cancelation to debounced search on destroy

* clean up

* add js docs

* two way binding of filter value

* clean up

* Add collection text filter manifest example

Introduced a new filter manifest for the example collection and updated the main manifests file to include it. This enables a text filter extension for the example collection.

* Delete unused element and context

* Update src/Umbraco.Web.UI.Client/src/packages/user/user-group/collection/user-group-collection.context-token.ts

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

* Update user-group-table-collection-view.element.ts

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-01-13 20:56:27 +01:00
CopilotGitHubcopilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>nielslyngsoeNiels Lyngsø
3342d31270 Add loading indicator and error handling to Member Public Access Modal (#21087)
* Initial plan

* Add loading indicator and error handling to public access modal

Co-authored-by: nielslyngsoe <6791648+nielslyngsoe@users.noreply.github.com>

* Fix test for public access modal element

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>
Co-authored-by: Niels Lyngsø <nsl@umbraco.dk>
2026-01-13 14:24:09 +00:00
Niels LyngsøandGitHub 7557f7bfa5 Content Type Designer: make inherited property appear more like the local, to take less focus (#21229)
make inherited property appear more like the local, otherwise it takes too much attention.
2026-01-13 13:40:54 +00:00
832 changed files with 29201 additions and 6941 deletions
@@ -12,6 +12,7 @@ permissions:
jobs:
reconcile:
if: github.repository == 'umbraco/Umbraco-CMS'
runs-on: ubuntu-latest
steps:
- name: Reconcile release/* labels → discussions
+1
View File
@@ -107,6 +107,7 @@ trace.zip
/src/Umbraco.Web.UI/appsettings-schema.json
/src/Umbraco.Web.UI/appsettings-schema.*.json
/src/Umbraco.Web.UI/umbraco-package-schema.json
/src/Umbraco.Web.UI.Client/umbraco-package-schema.json
/tests/Umbraco.Tests.Integration/appsettings-schema.json
/tests/Umbraco.Tests.Integration/appsettings-schema.*.json
/tests/Umbraco.Tests.Integration/umbraco-package-schema.json
+23 -23
View File
@@ -13,27 +13,27 @@
</ItemGroup>
<!-- Microsoft packages -->
<ItemGroup>
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation" Version="10.0.1" />
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation" Version="10.0.2" />
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="4.14.0" />
<PackageVersion Include="Microsoft.Data.Sqlite" Version="10.0.1" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="10.0.1" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.SqlServer" Version="10.0.1" />
<PackageVersion Include="Microsoft.Extensions.Caching.Abstractions" Version="10.0.1" />
<PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="10.0.1" />
<PackageVersion Include="Microsoft.Extensions.Configuration.Abstractions" Version="10.0.1" />
<PackageVersion Include="Microsoft.Extensions.Configuration.Json" Version="10.0.1" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="10.0.1" />
<PackageVersion Include="Microsoft.Extensions.FileProviders.Embedded" Version="10.0.1" />
<PackageVersion Include="Microsoft.Extensions.FileProviders.Physical" Version="10.0.1" />
<PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="10.0.1" />
<PackageVersion Include="Microsoft.Extensions.Http" Version="10.0.1" />
<PackageVersion Include="Microsoft.Extensions.Identity.Core" Version="10.0.1" />
<PackageVersion Include="Microsoft.Extensions.Identity.Stores" Version="10.0.1" />
<PackageVersion Include="Microsoft.Extensions.Logging" Version="10.0.1" />
<PackageVersion Include="Microsoft.Extensions.Options" Version="10.0.1" />
<PackageVersion Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="10.0.1" />
<PackageVersion Include="Microsoft.Extensions.Options.DataAnnotations" Version="10.0.1" />
<PackageVersion Include="Microsoft.Extensions.Caching.Hybrid" Version="10.1.0" />
<PackageVersion Include="Microsoft.Data.Sqlite" Version="10.0.2" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="10.0.2" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.SqlServer" Version="10.0.2" />
<PackageVersion Include="Microsoft.Extensions.Caching.Abstractions" Version="10.0.2" />
<PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="10.0.2" />
<PackageVersion Include="Microsoft.Extensions.Configuration.Abstractions" Version="10.0.2" />
<PackageVersion Include="Microsoft.Extensions.Configuration.Json" Version="10.0.2" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="10.0.2" />
<PackageVersion Include="Microsoft.Extensions.FileProviders.Embedded" Version="10.0.2" />
<PackageVersion Include="Microsoft.Extensions.FileProviders.Physical" Version="10.0.2" />
<PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="10.0.2" />
<PackageVersion Include="Microsoft.Extensions.Http" Version="10.0.2" />
<PackageVersion Include="Microsoft.Extensions.Identity.Core" Version="10.0.2" />
<PackageVersion Include="Microsoft.Extensions.Identity.Stores" Version="10.0.2" />
<PackageVersion Include="Microsoft.Extensions.Logging" Version="10.0.2" />
<PackageVersion Include="Microsoft.Extensions.Options" Version="10.0.2" />
<PackageVersion Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="10.0.2" />
<PackageVersion Include="Microsoft.Extensions.Options.DataAnnotations" Version="10.0.2" />
<PackageVersion Include="Microsoft.Extensions.Caching.Hybrid" Version="10.2.0" />
<PackageVersion Include="System.Linq.Async" Version="7.0.0" />
</ItemGroup>
<!-- Umbraco packages -->
@@ -42,8 +42,8 @@
</ItemGroup>
<!-- Third-party packages -->
<ItemGroup>
<PackageVersion Include="Asp.Versioning.Mvc" Version="8.1.0" />
<PackageVersion Include="Asp.Versioning.Mvc.ApiExplorer" Version="8.1.0" />
<PackageVersion Include="Asp.Versioning.Mvc" Version="8.1.1" />
<PackageVersion Include="Asp.Versioning.Mvc.ApiExplorer" Version="8.1.1" />
<PackageVersion Include="Dazinator.Extensions.FileProviders" Version="2.0.0" />
<PackageVersion Include="Examine" Version="3.7.1" />
<PackageVersion Include="Examine.Core" Version="3.7.1" />
@@ -76,7 +76,7 @@
<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" />
<PackageVersion Include="Swashbuckle.AspNetCore" Version="10.0.1" />
<PackageVersion Include="Swashbuckle.AspNetCore" Version="10.1.0" />
</ItemGroup>
<!-- Transitive pinned versions (only required because our direct dependencies have vulnerable versions of transitive dependencies) -->
<ItemGroup>
+77 -71
View File
@@ -201,53 +201,56 @@ stages:
umbracoMajorVersion: $[ stageDependencies.Build.A.outputs['build.NBGV_VersionMajor'] ]
jobs:
# C# API Reference
- job:
displayName: Build C# API Reference
pool:
vmImage: "windows-latest"
steps:
- task: UseDotNet@2
displayName: Use .NET SDK from global.json
inputs:
useGlobalJson: true
- task: PowerShell@2
displayName: Install DocFX
inputs:
targetType: inline
script: |
choco install docfx --version=2.59.4 -y
if ($lastexitcode -ne 0){
throw ("Error installing DocFX")
}
- task: PowerShell@2
displayName: Generate metadata
inputs:
targetType: inline
script: |
docfx metadata "$(Build.SourcesDirectory)/build/csharp-docs/docfx.json"
if ($lastexitcode -ne 0){
throw ("Error generating metadata.")
}
- task: PowerShell@2
displayName: Generate documentation
inputs:
targetType: inline
script: |
docfx build "$(Build.SourcesDirectory)/build/csharp-docs/docfx.json"
if ($lastexitcode -ne 0){
throw ("Error generating documentation.")
}
- task: ArchiveFiles@2
displayName: Archive C# Docs
inputs:
rootFolderOrFile: $(Build.SourcesDirectory)/build/csharp-docs/_site
includeRootFolder: false
archiveFile: $(Build.ArtifactStagingDirectory)/csharp-docs.zip
- task: PublishPipelineArtifact@1
displayName: Publish C# Docs
inputs:
targetPath: $(Build.ArtifactStagingDirectory)/csharp-docs.zip
artifact: csharp-docs
# This job is disabled due to failures in correct generation of the docs when running on the pipeline.
# API docs are currently manually generated locally and uploaded to blob storage.
# TODO: Investigate and resolve this issue such that doc generation and upload can be restored to the pipeline.
# - job:
# displayName: Build C# API Reference
# pool:
# vmImage: "windows-latest"
# steps:
# - task: UseDotNet@2
# displayName: Use .NET SDK from global.json
# inputs:
# useGlobalJson: true
# - task: PowerShell@2
# displayName: Install DocFX
# inputs:
# targetType: inline
# script: |
# choco install docfx --version=2.59.4 -y
# if ($lastexitcode -ne 0){
# throw ("Error installing DocFX")
# }
# - task: PowerShell@2
# displayName: Generate metadata
# inputs:
# targetType: inline
# script: |
# docfx metadata "$(Build.SourcesDirectory)/build/csharp-docs/docfx.json"
# if ($lastexitcode -ne 0){
# throw ("Error generating metadata.")
# }
# - task: PowerShell@2
# displayName: Generate documentation
# inputs:
# targetType: inline
# script: |
# docfx build "$(Build.SourcesDirectory)/build/csharp-docs/docfx.json"
# if ($lastexitcode -ne 0){
# throw ("Error generating documentation.")
# }
# - task: ArchiveFiles@2
# displayName: Archive C# Docs
# inputs:
# rootFolderOrFile: $(Build.SourcesDirectory)/build/csharp-docs/_site
# includeRootFolder: false
# archiveFile: $(Build.ArtifactStagingDirectory)/csharp-docs.zip
# - task: PublishPipelineArtifact@1
# displayName: Publish C# Docs
# inputs:
# targetPath: $(Build.ArtifactStagingDirectory)/csharp-docs.zip
# artifact: csharp-docs
# js API Reference
- job:
@@ -882,30 +885,33 @@ stages:
- Deploy_NuGet
condition: and(succeeded(), or(eq(dependencies.Build.outputs['A.build.NBGV_PublicRelease'], 'True'), ${{parameters.uploadApiDocs}}))
jobs:
- job:
displayName: Upload C# Docs
steps:
- checkout: none
- task: DownloadPipelineArtifact@2
displayName: Download artifact
inputs:
artifact: csharp-docs
path: $(Build.SourcesDirectory)
- task: ExtractFiles@1
inputs:
archiveFilePatterns: $(Build.SourcesDirectory)/csharp-docs.zip
destinationFolder: $(Build.ArtifactStagingDirectory)/csharp-docs
overwriteExistingFiles: true
- task: AzureFileCopy@4
displayName: "Copy C# Docs to blob storage"
inputs:
SourcePath: "$(Build.ArtifactStagingDirectory)/csharp-docs/*"
azureSubscription: umbraco-storage
Destination: AzureBlob
storage: umbracoapidocs
ContainerName: "$web"
BlobPrefix: v$(umbracoMajorVersion)/csharp
CleanTargetBeforeCopy: true
# This job is disabled due to failures in correct generation of the docs when running on the pipeline.
# API docs are currently manually generated locally and uploaded to blob storage.
# TODO: Investigate and resolve this issue such that doc generation and upload can be restored to the pipeline.
# - job:
# displayName: Upload C# Docs
# steps:
# - checkout: none
# - task: DownloadPipelineArtifact@2
# displayName: Download artifact
# inputs:
# artifact: csharp-docs
# path: $(Build.SourcesDirectory)
# - task: ExtractFiles@1
# inputs:
# archiveFilePatterns: $(Build.SourcesDirectory)/csharp-docs.zip
# destinationFolder: $(Build.ArtifactStagingDirectory)/csharp-docs
# overwriteExistingFiles: true
# - task: AzureFileCopy@4
# displayName: "Copy C# Docs to blob storage"
# inputs:
# SourcePath: "$(Build.ArtifactStagingDirectory)/csharp-docs/*"
# azureSubscription: umbraco-storage
# Destination: AzureBlob
# storage: umbracoapidocs
# ContainerName: "$web"
# BlobPrefix: v$(umbracoMajorVersion)/csharp
# CleanTargetBeforeCopy: true
- job:
displayName: Upload Storybook
steps:
@@ -1,10 +1,17 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http;
using Umbraco.Cms.Core.DeliveryApi;
namespace Umbraco.Cms.Api.Common.Accessors;
/// <summary>
/// Provides access to the <see cref="IOutputExpansionStrategy"/> for the current HTTP request context.
/// </summary>
public sealed class RequestContextOutputExpansionStrategyAccessor : RequestContextServiceAccessorBase<IOutputExpansionStrategy>, IOutputExpansionStrategyAccessor
{
/// <summary>
/// Initializes a new instance of the <see cref="RequestContextOutputExpansionStrategyAccessor"/> class.
/// </summary>
/// <param name="httpContextAccessor">The HTTP context accessor.</param>
public RequestContextOutputExpansionStrategyAccessor(IHttpContextAccessor httpContextAccessor)
: base(httpContextAccessor)
{
@@ -1,17 +1,30 @@
using System.Diagnostics.CodeAnalysis;
using System.Diagnostics.CodeAnalysis;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
namespace Umbraco.Cms.Api.Common.Accessors;
/// <summary>
/// Base class for accessing request-scoped services from the current HTTP context.
/// </summary>
/// <typeparam name="T">The type of service to access.</typeparam>
public abstract class RequestContextServiceAccessorBase<T>
where T : class
{
private readonly IHttpContextAccessor _httpContextAccessor;
/// <summary>
/// Initializes a new instance of the <see cref="RequestContextServiceAccessorBase{T}"/> class.
/// </summary>
/// <param name="httpContextAccessor">The HTTP context accessor.</param>
protected RequestContextServiceAccessorBase(IHttpContextAccessor httpContextAccessor)
=> _httpContextAccessor = httpContextAccessor;
/// <summary>
/// Attempts to retrieve the service from the current HTTP context's request services.
/// </summary>
/// <param name="requestStartNodeService">When this method returns, contains the service instance if found; otherwise, <c>null</c>.</param>
/// <returns><c>true</c> if the service was found; otherwise, <c>false</c>.</returns>
public bool TryGetValue([NotNullWhen(true)] out T? requestStartNodeService)
{
requestStartNodeService = _httpContextAccessor.HttpContext?.RequestServices.GetService<T>();
@@ -1,9 +1,19 @@
namespace Umbraco.Cms.Api.Common.Attributes;
/// <summary>
/// Attribute used to map a class to a specific API for OpenAPI documentation generation.
/// </summary>
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false)]
public class MapToApiAttribute : Attribute
{
/// <summary>
/// Initializes a new instance of the <see cref="MapToApiAttribute"/> class.
/// </summary>
/// <param name="apiName">The name of the API to map to.</param>
public MapToApiAttribute(string apiName) => ApiName = apiName;
/// <summary>
/// Gets the name of the API this class is mapped to.
/// </summary>
public string ApiName { get; }
}
@@ -1,9 +1,12 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Extensions;
namespace Umbraco.Cms.Api.Common.Builders;
/// <summary>
/// A fluent builder for creating RFC 7807 <see cref="ProblemDetails"/> responses.
/// </summary>
public class ProblemDetailsBuilder
{
private string? _title;
@@ -12,24 +15,45 @@ public class ProblemDetailsBuilder
private string? _operationStatus;
private IDictionary<string, object>? _extensions;
/// <summary>
/// Sets the title of the problem details.
/// </summary>
/// <param name="title">A short, human-readable summary of the problem type.</param>
/// <returns>The current builder instance for method chaining.</returns>
public ProblemDetailsBuilder WithTitle(string title)
{
_title = title;
return this;
}
/// <summary>
/// Sets the detail of the problem details.
/// </summary>
/// <param name="detail">A human-readable explanation specific to this occurrence of the problem.</param>
/// <returns>The current builder instance for method chaining.</returns>
public ProblemDetailsBuilder WithDetail(string detail)
{
_detail = detail;
return this;
}
/// <summary>
/// Sets the type of the problem details.
/// </summary>
/// <param name="type">A URI reference that identifies the problem type.</param>
/// <returns>The current builder instance for method chaining.</returns>
public ProblemDetailsBuilder WithType(string type)
{
_type = type;
return this;
}
/// <summary>
/// Sets the operation status from an enum value.
/// </summary>
/// <typeparam name="TEnum">The enum type representing operation statuses.</typeparam>
/// <param name="operationStatus">The operation status enum value.</param>
/// <returns>The current builder instance for method chaining.</returns>
public ProblemDetailsBuilder WithOperationStatus<TEnum>(TEnum operationStatus)
where TEnum : Enum
{
@@ -37,9 +61,20 @@ public class ProblemDetailsBuilder
return this;
}
/// <summary>
/// Adds request model validation errors to the problem details.
/// </summary>
/// <param name="errors">A dictionary of field names to error messages.</param>
/// <returns>The current builder instance for method chaining.</returns>
public ProblemDetailsBuilder WithRequestModelErrors(IDictionary<string, string[]> errors)
=> WithExtension(nameof(HttpValidationProblemDetails.Errors).ToFirstLowerInvariant(), errors);
/// <summary>
/// Adds a custom extension to the problem details.
/// </summary>
/// <param name="key">The extension key.</param>
/// <param name="value">The extension value.</param>
/// <returns>The current builder instance for method chaining.</returns>
public ProblemDetailsBuilder WithExtension(string key, object value)
{
_extensions ??= new Dictionary<string, object>();
@@ -47,6 +82,10 @@ public class ProblemDetailsBuilder
return this;
}
/// <summary>
/// Builds the <see cref="ProblemDetails"/> instance with all configured values.
/// </summary>
/// <returns>A new <see cref="ProblemDetails"/> instance.</returns>
public ProblemDetails Build()
{
var problemDetails = new ProblemDetails
@@ -1,10 +1,14 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options;
namespace Umbraco.Cms.Api.Common.Configuration;
/// <summary>
/// Configures <see cref="ApiBehaviorOptions"/> for Umbraco APIs.
/// </summary>
public class ConfigureApiBehaviorOptions : IConfigureOptions<ApiBehaviorOptions>
{
/// <inheritdoc/>
public void Configure(ApiBehaviorOptions options) =>
// disable ProblemDetails as default result type for every non-success response (i.e. 404)
// - see https://learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.mvc.apibehavioroptions.suppressmapclienterrors
@@ -5,12 +5,21 @@ using Umbraco.Cms.Api.Common.Json;
namespace Umbraco.Cms.Api.Common.Configuration;
/// <summary>
/// Configures <see cref="MvcOptions"/> with named JSON input and output formatters for Umbraco APIs.
/// </summary>
public class ConfigureMvcJsonOptions : IConfigureOptions<MvcOptions>
{
private readonly string _jsonOptionsName;
private readonly IOptionsMonitor<JsonOptions> _jsonOptions;
private readonly ILoggerFactory _loggerFactory;
/// <summary>
/// Initializes a new instance of the <see cref="ConfigureMvcJsonOptions"/> class.
/// </summary>
/// <param name="jsonOptionsName">The name of the JSON options configuration to use.</param>
/// <param name="jsonOptions">The JSON options monitor.</param>
/// <param name="loggerFactory">The logger factory.</param>
public ConfigureMvcJsonOptions(
string jsonOptionsName,
IOptionsMonitor<JsonOptions> jsonOptions,
@@ -21,6 +30,7 @@ public class ConfigureMvcJsonOptions : IConfigureOptions<MvcOptions>
_loggerFactory = loggerFactory;
}
/// <inheritdoc/>
public void Configure(MvcOptions options)
{
JsonOptions jsonOptions = _jsonOptions.Get(_jsonOptionsName);
@@ -4,12 +4,24 @@ using Umbraco.Cms.Core.Configuration.Models;
namespace Umbraco.Cms.Api.Common.Configuration;
/// <summary>
/// Configures OpenIddict server options for Umbraco authentication.
/// </summary>
/// <remarks>
/// Disables transport security requirement when HTTPS is not configured in global settings.
/// Warning: This should only be used in development environments.
/// </remarks>
internal sealed class ConfigureOpenIddict : IConfigureOptions<OpenIddictServerAspNetCoreOptions>
{
private readonly IOptions<GlobalSettings> _globalSettings;
/// <summary>
/// Initializes a new instance of the <see cref="ConfigureOpenIddict"/> class.
/// </summary>
/// <param name="globalSettings">The global settings options.</param>
public ConfigureOpenIddict(IOptions<GlobalSettings> globalSettings) => _globalSettings = globalSettings;
/// <inheritdoc/>
public void Configure(OpenIddictServerAspNetCoreOptions options)
=> options.DisableTransportSecurityRequirement = _globalSettings.Value.UseHttps is false;
}
@@ -8,6 +8,9 @@ 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;
@@ -15,6 +18,13 @@ public class ConfigureUmbracoSwaggerGenOptions : IConfigureOptions<SwaggerGenOpt
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,
@@ -27,6 +37,12 @@ public class ConfigureUmbracoSwaggerGenOptions : IConfigureOptions<SwaggerGenOpt
_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,
@@ -40,6 +56,7 @@ public class ConfigureUmbracoSwaggerGenOptions : IConfigureOptions<SwaggerGenOpt
{
}
/// <inheritdoc/>
public void Configure(SwaggerGenOptions swaggerGenOptions)
{
swaggerGenOptions.SwaggerDoc(
@@ -64,7 +81,14 @@ public class ConfigureUmbracoSwaggerGenOptions : IConfigureOptions<SwaggerGenOpt
swaggerGenOptions.SupportNonNullableReferenceTypes();
}
// see https://github.com/domaindrivendev/Swashbuckle.AspNetCore#change-operation-sort-order-eg-for-ui-sorting
/// <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}";
}
@@ -1,6 +1,12 @@
namespace Umbraco.Cms.Api.Common.Configuration;
/// <summary>
/// Contains default configuration values for the API.
/// </summary>
internal static class DefaultApiConfiguration
{
/// <summary>
/// The default API name used for endpoints not assigned to a specific API.
/// </summary>
public const string ApiName = "default";
}
@@ -13,6 +13,14 @@ using Umbraco.Extensions;
namespace Umbraco.Cms.Api.Common.DependencyInjection;
/// <summary>
/// Handles secure storage of back-office authentication tokens in HTTP-only cookies.
/// </summary>
/// <remarks>
/// This handler intercepts OpenIddict token responses for the back-office client and stores
/// access tokens, refresh tokens, and PKCE codes in encrypted HTTP-only cookies. The tokens
/// are redacted from the response to prevent client-side JavaScript access.
/// </remarks>
internal sealed class HideBackOfficeTokensHandler
: IOpenIddictServerHandler<OpenIddictServerEvents.ApplyTokenResponseContext>,
IOpenIddictServerHandler<OpenIddictServerEvents.ApplyAuthorizationResponseContext>,
@@ -34,6 +42,13 @@ internal sealed class HideBackOfficeTokensHandler
private readonly BackOfficeTokenCookieSettings _backOfficeTokenCookieSettings;
private readonly GlobalSettings _globalSettings;
/// <summary>
/// Initializes a new instance of the <see cref="HideBackOfficeTokensHandler"/> class.
/// </summary>
/// <param name="httpContextAccessor">The HTTP context accessor.</param>
/// <param name="dataProtectionProvider">The data protection provider for encrypting cookie values.</param>
/// <param name="backOfficeTokenCookieSettings">The back-office token cookie settings.</param>
/// <param name="globalSettings">The global settings.</param>
public HideBackOfficeTokensHandler(
IHttpContextAccessor httpContextAccessor,
IDataProtectionProvider dataProtectionProvider,
@@ -163,6 +178,7 @@ internal sealed class HideBackOfficeTokensHandler
return ValueTask.CompletedTask;
}
/// <inheritdoc/>
public void Handle(UserLogoutSuccessNotification notification)
{
HttpContext? httpContext = _httpContextAccessor.HttpContext;
@@ -1,4 +1,4 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
@@ -6,8 +6,18 @@ using Umbraco.Cms.Api.Common.Configuration;
namespace Umbraco.Cms.Api.Common.DependencyInjection;
/// <summary>
/// Extension methods for <see cref="IMvcBuilder"/>.
/// </summary>
public static class MvcBuilderExtensions
{
/// <summary>
/// Adds named JSON serialization options to the MVC builder.
/// </summary>
/// <param name="builder">The MVC builder.</param>
/// <param name="settingsName">The name for the JSON options configuration.</param>
/// <param name="configure">The action to configure the JSON options.</param>
/// <returns>The MVC builder for method chaining.</returns>
public static IMvcBuilder AddJsonOptions(this IMvcBuilder builder, string settingsName, Action<JsonOptions> configure)
{
builder.Services.Configure(settingsName, configure);
@@ -1,4 +1,4 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http;
using OpenIddict.Server;
using OpenIddict.Validation;
using Umbraco.Cms.Core;
@@ -6,12 +6,23 @@ using Umbraco.Extensions;
namespace Umbraco.Cms.Api.Common.DependencyInjection;
/// <summary>
/// Handles OpenIddict request processing to skip handling for non-authentication requests.
/// </summary>
/// <remarks>
/// This handler prevents OpenIddict from processing every request to the server,
/// limiting its scope to back-office and well-known OpenID Connect endpoints.
/// </remarks>
public class ProcessRequestContextHandler
: IOpenIddictServerHandler<OpenIddictServerEvents.ProcessRequestContext>, IOpenIddictValidationHandler<OpenIddictValidationEvents.ProcessRequestContext>
{
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly string[] _pathsToHandle;
/// <summary>
/// Initializes a new instance of the <see cref="ProcessRequestContextHandler"/> class.
/// </summary>
/// <param name="httpContextAccessor">The HTTP context accessor.</param>
public ProcessRequestContextHandler(IHttpContextAccessor httpContextAccessor)
{
_httpContextAccessor = httpContextAccessor;
@@ -21,6 +32,11 @@ public class ProcessRequestContextHandler
_pathsToHandle = [backOfficePathSegment, "/.well-known/openid-configuration", "/.well-known/jwks"];
}
/// <summary>
/// Handles the server process request context event.
/// </summary>
/// <param name="context">The process request context.</param>
/// <returns>A <see cref="ValueTask"/> representing the asynchronous operation.</returns>
public ValueTask HandleAsync(OpenIddictServerEvents.ProcessRequestContext context)
{
if (SkipOpenIddictHandlingForRequest())
@@ -31,6 +47,11 @@ public class ProcessRequestContextHandler
return ValueTask.CompletedTask;
}
/// <summary>
/// Handles the validation process request context event.
/// </summary>
/// <param name="context">The process request context.</param>
/// <returns>A <see cref="ValueTask"/> representing the asynchronous operation.</returns>
public ValueTask HandleAsync(OpenIddictValidationEvents.ProcessRequestContext context)
{
if (SkipOpenIddictHandlingForRequest())
@@ -7,8 +7,16 @@ using Umbraco.Cms.Web.Common.ApplicationBuilder;
namespace Umbraco.Cms.Api.Common.DependencyInjection;
/// <summary>
/// Extension methods for <see cref="IUmbracoBuilder"/> to configure API services.
/// </summary>
public static class UmbracoBuilderApiExtensions
{
/// <summary>
/// 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)
{
if (builder.Services.Any(x => !x.IsKeyedService && x.ImplementationType == typeof(OperationIdSelector)))
@@ -9,14 +9,26 @@ using Umbraco.Cms.Api.Common.Security;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Configuration.Models;
using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Core.Notifications;
using Umbraco.Cms.Infrastructure.BackgroundJobs;
using Umbraco.Cms.Infrastructure.BackgroundJobs.Jobs.DistributedJobs;
using Umbraco.Cms.Core.Notifications;
namespace Umbraco.Cms.Api.Common.DependencyInjection;
/// <summary>
/// Extension methods for <see cref="IUmbracoBuilder"/> to configure authentication services.
/// </summary>
public static class UmbracoBuilderAuthExtensions
{
/// <summary>
/// Adds OpenIddict authentication services for Umbraco APIs.
/// </summary>
/// <param name="builder">The Umbraco builder.</param>
/// <returns>The Umbraco builder for method chaining.</returns>
/// <remarks>
/// Configures OpenIddict with authorization code flow (with PKCE), client credentials flow,
/// reference tokens, and ASP.NET Core Data Protection for token encryption.
/// </remarks>
public static IUmbracoBuilder AddUmbracoOpenIddict(this IUmbracoBuilder builder)
{
if (builder.Services.Any(x => !x.IsKeyedService && x.ImplementationType == typeof(OpenIddictCleanupJob)) is false)
@@ -33,4 +33,4 @@ public static class ActionDescriptorApiCommonExtensions
return mapToApiAttributes.SingleOrDefault()?.ApiName;
}
}
}
@@ -5,9 +5,16 @@ using Umbraco.Cms.Api.Common.Configuration;
namespace Umbraco.Extensions;
/// <summary>
/// Extension methods for <see cref="MethodInfo"/> to work with API-related attributes.
/// </summary>
public static class MethodInfoApiCommonExtensions
{
/// <summary>
/// Gets the API version values from <see cref="MapToApiVersionAttribute"/> applied to the method.
/// </summary>
/// <param name="methodInfo">The method info to inspect.</param>
/// <returns>A pipe-separated string of API version values.</returns>
public static string GetMapToApiVersionAttributeValue(this MethodInfo methodInfo)
{
MapToApiVersionAttribute[] mapToApis = methodInfo.GetCustomAttributes(typeof(MapToApiVersionAttribute), inherit: true).Cast<MapToApiVersionAttribute>().ToArray();
@@ -15,6 +22,11 @@ public static class MethodInfoApiCommonExtensions
return string.Join("|", mapToApis.SelectMany(x => x.Versions));
}
/// <summary>
/// Gets the API name from <see cref="MapToApiAttribute"/> applied to the method's declaring type.
/// </summary>
/// <param name="methodInfo">The method info to inspect.</param>
/// <returns>The API name if the attribute is present; otherwise, <c>null</c>.</returns>
public static string? GetMapToApiAttributeValue(this MethodInfo methodInfo)
{
MapToApiAttribute[] mapToApis = (methodInfo.DeclaringType?.GetCustomAttributes(typeof(MapToApiAttribute), inherit: true) ?? Array.Empty<object>()).Cast<MapToApiAttribute>().ToArray();
@@ -22,6 +34,15 @@ public static class MethodInfoApiCommonExtensions
return mapToApis.SingleOrDefault()?.ApiName;
}
/// <summary>
/// Determines whether the method's declaring type has a <see cref="MapToApiAttribute"/> with the specified API name.
/// </summary>
/// <param name="methodInfo">The method info to inspect.</param>
/// <param name="apiName">The API name to check for.</param>
/// <returns>
/// <c>true</c> if the attribute is present and matches the specified API name,
/// or if the attribute is not present and the API name matches the default API name; otherwise, <c>false</c>.
/// </returns>
public static bool HasMapToApiAttribute(this MethodInfo methodInfo, string apiName)
{
var value = methodInfo.GetMapToApiAttributeValue();
@@ -1,9 +1,19 @@
namespace Umbraco.Cms.Api.Common.Filters;
namespace Umbraco.Cms.Api.Common.Filters;
/// <summary>
/// Attribute used to specify the named JSON serialization options for a controller.
/// </summary>
[AttributeUsage(AttributeTargets.Class)]
public class JsonOptionsNameAttribute : Attribute
{
/// <summary>
/// Initializes a new instance of the <see cref="JsonOptionsNameAttribute"/> class.
/// </summary>
/// <param name="jsonOptionsName">The name of the JSON options configuration to use.</param>
public JsonOptionsNameAttribute(string jsonOptionsName) => JsonOptionsName = jsonOptionsName;
/// <summary>
/// Gets the name of the JSON options configuration.
/// </summary>
public string JsonOptionsName { get; }
}
@@ -1,10 +1,18 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http;
using Umbraco.Cms.Api.Common.Filters;
namespace Umbraco.Cms.Api.Common.Json;
/// <summary>
/// Extension methods for <see cref="HttpContext"/> related to JSON serialization.
/// </summary>
public static class HttpContextJsonExtensions
{
/// <summary>
/// Gets the named JSON options configuration for the current endpoint.
/// </summary>
/// <param name="context">The HTTP context.</param>
/// <returns>The JSON options name if specified via <see cref="JsonOptionsNameAttribute"/>; otherwise, <c>null</c>.</returns>
public static string? CurrentJsonOptionsName(this HttpContext context)
=> context.GetEndpoint()?.Metadata.GetMetadata<JsonOptionsNameAttribute>()?.JsonOptionsName;
}
@@ -1,20 +1,31 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Formatters;
using Microsoft.Extensions.Logging;
namespace Umbraco.Cms.Api.Common.Json;
/// <summary>
/// A JSON input formatter that only processes requests for endpoints with matching named JSON options.
/// </summary>
internal sealed class NamedSystemTextJsonInputFormatter : SystemTextJsonInputFormatter
{
private readonly string _jsonOptionsName;
/// <summary>
/// Initializes a new instance of the <see cref="NamedSystemTextJsonInputFormatter"/> class.
/// </summary>
/// <param name="jsonOptionsName">The name of the JSON options configuration this formatter handles.</param>
/// <param name="options">The JSON options.</param>
/// <param name="logger">The logger.</param>
public NamedSystemTextJsonInputFormatter(string jsonOptionsName, JsonOptions options, ILogger<NamedSystemTextJsonInputFormatter> logger)
: base(options, logger) =>
_jsonOptionsName = jsonOptionsName;
/// <inheritdoc/>
public override bool CanRead(InputFormatterContext context)
=> context.HttpContext.CurrentJsonOptionsName() == _jsonOptionsName && base.CanRead(context);
/// <inheritdoc/>
public override async Task<InputFormatterResult> ReadAsync(InputFormatterContext context)
{
try
@@ -1,17 +1,26 @@
using System.Text.Json;
using System.Text.Json;
using Microsoft.AspNetCore.Mvc.Formatters;
namespace Umbraco.Cms.Api.Common.Json;
/// <summary>
/// A JSON output formatter that only processes responses for endpoints with matching named JSON options.
/// </summary>
internal sealed class NamedSystemTextJsonOutputFormatter : SystemTextJsonOutputFormatter
{
private readonly string _jsonOptionsName;
/// <summary>
/// Initializes a new instance of the <see cref="NamedSystemTextJsonOutputFormatter"/> class.
/// </summary>
/// <param name="jsonOptionsName">The name of the JSON options configuration this formatter handles.</param>
/// <param name="jsonSerializerOptions">The JSON serializer options.</param>
public NamedSystemTextJsonOutputFormatter(string jsonOptionsName, JsonSerializerOptions jsonSerializerOptions) : base(jsonSerializerOptions)
{
_jsonOptionsName = jsonOptionsName;
}
/// <inheritdoc/>
public override bool CanWriteResult(OutputFormatterCanWriteContext context)
=> context.HttpContext.CurrentJsonOptionsName() == _jsonOptionsName && base.CanWriteResult(context);
}
@@ -1,4 +1,4 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Routing;
using Microsoft.Extensions.DependencyInjection;
@@ -16,6 +16,13 @@ public sealed class EmptyCreatedAtActionResult : ActionResult
private readonly object _routeValues;
private readonly string _resourceIdentifier;
/// <summary>
/// Initializes a new instance of the <see cref="EmptyCreatedAtActionResult"/> class.
/// </summary>
/// <param name="actionName">The name of the action to generate the URL for.</param>
/// <param name="controllerName">The name of the controller to generate the URL for.</param>
/// <param name="routeValues">The route values to use for URL generation.</param>
/// <param name="resourceIdentifier">The identifier of the created resource.</param>
public EmptyCreatedAtActionResult(string actionName, string controllerName, object routeValues, string resourceIdentifier)
{
_actionName = actionName;
@@ -24,6 +31,7 @@ public sealed class EmptyCreatedAtActionResult : ActionResult
_resourceIdentifier = resourceIdentifier;
}
/// <inheritdoc/>
public override void ExecuteResult(ActionContext context)
{
ArgumentNullException.ThrowIfNull(context);
@@ -1,4 +1,4 @@
using System.Reflection;
using System.Reflection;
using System.Runtime.Serialization;
using System.Text.Json.Nodes;
using Microsoft.OpenApi;
@@ -6,8 +6,16 @@ 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)
@@ -2,9 +2,22 @@ using Microsoft.AspNetCore.Mvc.ApiExplorer;
namespace Umbraco.Cms.Api.Common.OpenApi;
/// <summary>
/// Defines a handler for generating OpenAPI operation IDs.
/// </summary>
public interface IOperationIdHandler
{
bool CanHandle(ApiDescription apiDescription);
/// <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);
string Handle(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);
}
@@ -3,7 +3,15 @@ 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,8 +1,21 @@
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,6 +1,14 @@
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,8 +1,22 @@
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,6 +1,14 @@
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,4 +1,4 @@
using Microsoft.OpenApi;
using Microsoft.OpenApi;
using Swashbuckle.AspNetCore.SwaggerGen;
using Umbraco.Extensions;
@@ -11,8 +11,13 @@ 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)
@@ -24,7 +29,7 @@ public class MimeTypeDocumentFilter : IDocumentFilter
.SelectMany(path => path.Value.Operations?.Values ?? Enumerable.Empty<OpenApiOperation>())
.ToArray();
void RemoveUnwantedMimeTypes(IDictionary<string, OpenApiMediaType>? content)
static void RemoveUnwantedMimeTypes(IDictionary<string, OpenApiMediaType>? content)
{
if (content is null || content.ContainsKey("application/json") is false)
{
@@ -6,14 +6,24 @@ using Umbraco.Extensions;
namespace Umbraco.Cms.Api.Common.OpenApi;
// NOTE: Left unsealed on purpose, so it is extendable.
/// <summary>
/// Default handler for generating OpenAPI operation IDs for Umbraco API controllers.
/// </summary>
/// <remarks>
/// Left unsealed on purpose, so it is extendable by consuming APIs.
/// </remarks>
public class OperationIdHandler : IOperationIdHandler
{
private readonly ApiVersioningOptions _apiVersioningOptions;
/// <summary>
/// Initializes a new instance of the <see cref="OperationIdHandler"/> class.
/// </summary>
/// <param name="apiVersioningOptions">The API versioning options.</param>
public OperationIdHandler(IOptions<ApiVersioningOptions> apiVersioningOptions)
=> _apiVersioningOptions = apiVersioningOptions.Value;
/// <inheritdoc/>
public bool CanHandle(ApiDescription apiDescription)
{
if (apiDescription.ActionDescriptor is not ControllerActionDescriptor controllerActionDescriptor)
@@ -24,9 +34,16 @@ public class OperationIdHandler : IOperationIdHandler
return CanHandle(apiDescription, controllerActionDescriptor);
}
/// <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);
@@ -1,4 +1,4 @@
using System.Text.RegularExpressions;
using System.Text.RegularExpressions;
namespace Umbraco.Cms.Api.Common.OpenApi;
@@ -3,19 +3,30 @@ 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. This will be removed in Umbraco 15.")]
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));
@@ -1,4 +1,4 @@
using Microsoft.OpenApi;
using Microsoft.OpenApi;
using Swashbuckle.AspNetCore.SwaggerGen;
namespace Umbraco.Cms.Api.Common.OpenApi;
@@ -10,9 +10,14 @@ 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)
@@ -3,12 +3,20 @@ using Umbraco.Extensions;
namespace Umbraco.Cms.Api.Common.OpenApi;
// NOTE: Left unsealed on purpose, so it is extendable.
/// <summary>
/// Default handler for generating OpenAPI schema IDs for Umbraco types.
/// </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
{
/// <inheritdoc/>
public virtual bool CanHandle(Type type)
=> type.Namespace?.StartsWith("Umbraco.Cms") is true;
/// <inheritdoc/>
public virtual string Handle(Type type)
=> UmbracoSchemaId(type);
@@ -1,12 +1,20 @@
namespace Umbraco.Cms.Api.Common.OpenApi;
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));
@@ -2,19 +2,33 @@ 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);
}
@@ -8,6 +8,9 @@ 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;
@@ -15,6 +18,13 @@ public class SubTypesSelector : ISubTypesSelector
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,
@@ -27,6 +37,7 @@ public class SubTypesSelector : ISubTypesSelector
_umbracoJsonTypeInfoResolver = umbracoJsonTypeInfoResolver;
}
/// <inheritdoc/>
public IEnumerable<Type> SubTypes(Type type)
{
var backOfficePath = _hostingEnvironment.GetBackOfficePath();
@@ -35,8 +46,7 @@ public class SubTypesSelector : ISubTypesSelector
if (_httpContextAccessor.HttpContext?.Request.Path.StartsWithSegments(swaggerPath) ?? false)
{
// Split the path into segments
var segments = _httpContextAccessor.HttpContext.Request.Path.Value!
.Substring(swaggerPath.Length)
var segments = _httpContextAccessor.HttpContext.Request.Path.Value![swaggerPath.Length..]
.TrimStart(Constants.CharArrays.ForwardSlash)
.Split(Constants.CharArrays.ForwardSlash);
@@ -13,8 +13,15 @@ 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;
@@ -36,15 +43,36 @@ public class SwaggerRouteTemplatePipelineFilter : UmbracoPipelineFilter
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,
@@ -4,30 +4,64 @@ using Umbraco.Extensions;
namespace Umbraco.Cms.Api.Common.Rendering;
/// <summary>
/// Implements output expansion strategy for element-only rendering in the Delivery API.
/// </summary>
/// <remarks>
/// This strategy handles the expansion and filtering of properties when rendering content
/// through the Delivery API based on expand and fields query parameters.
/// </remarks>
public class ElementOnlyOutputExpansionStrategy : IOutputExpansionStrategy
{
/// <summary>
/// The parameter value indicating all properties should be included.
/// </summary>
protected const string All = "$all";
/// <summary>
/// The parameter value indicating no properties should be included.
/// </summary>
protected const string None = "";
/// <summary>
/// The name of the expand query parameter.
/// </summary>
protected const string ExpandParameterName = "expand";
/// <summary>
/// The name of the fields query parameter.
/// </summary>
protected const string FieldsParameterName = "fields";
private readonly IApiPropertyRenderer _propertyRenderer;
/// <summary>
/// Gets the stack of expand property nodes for tracking nested expansions.
/// </summary>
protected Stack<Node?> ExpandProperties { get; } = new();
/// <summary>
/// Gets the stack of include property nodes for tracking nested field selections.
/// </summary>
protected Stack<Node?> IncludeProperties { get; } = new();
/// <summary>
/// Initializes a new instance of the <see cref="ElementOnlyOutputExpansionStrategy"/> class.
/// </summary>
/// <param name="propertyRenderer">The property renderer for converting property values.</param>
public ElementOnlyOutputExpansionStrategy(
IApiPropertyRenderer propertyRenderer)
{
_propertyRenderer = propertyRenderer;
}
/// <inheritdoc/>
public virtual IDictionary<string, object?> MapContentProperties(IPublishedContent content)
=> content.ItemType == PublishedItemType.Content
? MapProperties(content.Properties)
: throw new ArgumentException($"Invalid item type. This method can only be used with item type {nameof(PublishedItemType.Content)}, got: {content.ItemType}");
/// <inheritdoc/>
public virtual IDictionary<string, object?> MapMediaProperties(IPublishedContent media, bool skipUmbracoProperties = true)
{
if (media.ItemType != PublishedItemType.Media)
@@ -45,6 +79,7 @@ public class ElementOnlyOutputExpansionStrategy : IOutputExpansionStrategy
: new Dictionary<string, object?>();
}
/// <inheritdoc/>
public virtual IDictionary<string, object?> MapElementProperties(IPublishedElement element)
=> MapProperties(element.Properties, true);
@@ -87,12 +122,27 @@ public class ElementOnlyOutputExpansionStrategy : IOutputExpansionStrategy
private object? GetPropertyValue(IPublishedProperty property)
=> _propertyRenderer.GetPropertyValue(property, ExpandProperties.Peek() is not null);
/// <summary>
/// Represents a node in the parsed expand/fields parameter tree structure.
/// </summary>
protected sealed class Node
{
/// <summary>
/// Gets the key of this node.
/// </summary>
public string Key { get; private set; } = string.Empty;
/// <summary>
/// Gets the child nodes of this node.
/// </summary>
public List<Node> Items { get; } = new();
/// <summary>
/// Parses an expand/fields parameter value into a node tree structure.
/// </summary>
/// <param name="value">The parameter value to parse.</param>
/// <returns>The root node of the parsed tree.</returns>
/// <exception cref="ArgumentException">Thrown when the value has invalid syntax.</exception>
public static Node Parse(string value)
{
// verify that there are as many start brackets as there are end brackets
@@ -2,35 +2,77 @@ using Umbraco.Cms.Core;
namespace Umbraco.Cms.Api.Common.Security;
/// <summary>
/// Contains OAuth/OpenID Connect endpoint paths for Umbraco APIs.
/// </summary>
public static class Paths
{
/// <summary>
/// Contains endpoint paths for back-office authentication.
/// </summary>
public static class BackOfficeApi
{
/// <summary>
/// The base endpoint template for back-office security endpoints.
/// </summary>
public const string EndpointTemplate = "security/back-office";
/// <summary>
/// The authorization endpoint path.
/// </summary>
public static readonly string AuthorizationEndpoint = EndpointPath($"{EndpointTemplate}/authorize");
/// <summary>
/// The token endpoint path.
/// </summary>
public static readonly string TokenEndpoint = EndpointPath($"{EndpointTemplate}/token");
/// <summary>
/// The logout/sign-out endpoint path.
/// </summary>
public static readonly string LogoutEndpoint = EndpointPath($"{EndpointTemplate}/signout");
/// <summary>
/// The token revocation endpoint path.
/// </summary>
public static readonly string RevokeEndpoint = EndpointPath($"{EndpointTemplate}/revoke");
private static string EndpointPath(string relativePath) => $"/umbraco{Constants.Web.ManagementApiPath}v1/{relativePath}";
}
/// <summary>
/// Contains endpoint paths for member authentication.
/// </summary>
public static class MemberApi
{
/// <summary>
/// The base endpoint template for member security endpoints.
/// </summary>
public const string EndpointTemplate = "security/member";
/// <summary>
/// The authorization endpoint path.
/// </summary>
public static readonly string AuthorizationEndpoint = EndpointPath($"{EndpointTemplate}/authorize");
/// <summary>
/// The token endpoint path.
/// </summary>
public static readonly string TokenEndpoint = EndpointPath($"{EndpointTemplate}/token");
/// <summary>
/// The logout/sign-out endpoint path.
/// </summary>
public static readonly string LogoutEndpoint = EndpointPath($"{EndpointTemplate}/signout");
/// <summary>
/// The token revocation endpoint path.
/// </summary>
public static readonly string RevokeEndpoint = EndpointPath($"{EndpointTemplate}/revoke");
/// <summary>
/// The user info endpoint path.
/// </summary>
public static readonly string UserinfoEndpoint = EndpointPath($"{EndpointTemplate}/userinfo");
// NOTE: we're NOT using /api/v1.0/ here because it will clash with the Delivery API docs
@@ -2,9 +2,22 @@ using System.Text.Json.Serialization.Metadata;
namespace Umbraco.Cms.Api.Common.Serialization;
/// <summary>
/// Extends <see cref="IJsonTypeInfoResolver"/> with Umbraco-specific type resolution for polymorphic JSON serialization.
/// </summary>
public interface IUmbracoJsonTypeInfoResolver : IJsonTypeInfoResolver
{
/// <summary>
/// Finds all sub-types of the specified type for polymorphic serialization.
/// </summary>
/// <param name="type">The base type to find sub-types for.</param>
/// <returns>An enumerable of sub-types.</returns>
IEnumerable<Type> FindSubTypes(Type type);
/// <summary>
/// Gets the type discriminator value used for polymorphic serialization.
/// </summary>
/// <param name="type">The type to get the discriminator value for.</param>
/// <returns>The discriminator value, or <c>null</c> if not applicable.</returns>
string? GetTypeDiscriminatorValue(Type type);
}
@@ -8,14 +8,26 @@ using Umbraco.Extensions;
namespace Umbraco.Cms.Api.Common.Serialization;
/// <summary>
/// Implements JSON type info resolution for Umbraco with support for polymorphic serialization.
/// </summary>
/// <remarks>
/// This resolver discovers sub-types of interfaces for polymorphic JSON serialization,
/// caching results for performance. It also handles type discriminator values for OpenAPI schema generation.
/// </remarks>
public sealed class UmbracoJsonTypeInfoResolver : DefaultJsonTypeInfoResolver, IUmbracoJsonTypeInfoResolver
{
private readonly ITypeFinder _typeFinder;
private readonly ConcurrentDictionary<Type, ISet<Type>> _subTypesCache = new ConcurrentDictionary<Type, ISet<Type>>();
/// <summary>
/// Initializes a new instance of the <see cref="UmbracoJsonTypeInfoResolver"/> class.
/// </summary>
/// <param name="typeFinder">The type finder for discovering sub-types.</param>
public UmbracoJsonTypeInfoResolver(ITypeFinder typeFinder)
=> _typeFinder = typeFinder;
/// <inheritdoc/>
public IEnumerable<Type> FindSubTypes(Type type)
{
JsonDerivedTypeAttribute[] explicitJsonDerivedTypes = type
@@ -44,6 +56,7 @@ public sealed class UmbracoJsonTypeInfoResolver : DefaultJsonTypeInfoResolver, I
return result;
}
/// <inheritdoc/>
public string? GetTypeDiscriminatorValue(Type type)
{
JsonDerivedTypeAttribute? jsonDerivedTypeAttribute = type
@@ -62,6 +75,7 @@ public sealed class UmbracoJsonTypeInfoResolver : DefaultJsonTypeInfoResolver, I
return typeof(IOpenApiDiscriminator).IsAssignableFrom(type) ? type.Name : null;
}
/// <inheritdoc/>
public override JsonTypeInfo GetTypeInfo(Type type, JsonSerializerOptions options)
{
JsonTypeInfo result = base.GetTypeInfo(type, options);
@@ -1,14 +1,28 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations;
namespace Umbraco.Cms.Api.Common.ViewModels.Pagination;
/// <summary>
/// Represents a paged collection of items with total count.
/// </summary>
/// <typeparam name="T">The type of items in the collection.</typeparam>
public class PagedViewModel<T>
{
/// <summary>
/// Gets or sets the total number of items available.
/// </summary>
[Required]
public long Total { get; set; }
/// <summary>
/// Gets or sets the items in the current page.
/// </summary>
[Required]
public IEnumerable<T> Items { get; set; } = Enumerable.Empty<T>();
/// <summary>
/// Creates an empty paged view model.
/// </summary>
/// <returns>An empty <see cref="PagedViewModel{T}"/> instance.</returns>
public static PagedViewModel<T> Empty() => new();
}
@@ -2,16 +2,33 @@ using System.ComponentModel.DataAnnotations;
namespace Umbraco.Cms.Api.Common.ViewModels.Pagination;
/// <summary>
/// Represents a subset of items with counts of items before and after the subset.
/// </summary>
/// <typeparam name="T">The type of items in the collection.</typeparam>
public class SubsetViewModel<T>
{
/// <summary>
/// Gets or sets the total number of items before this subset.
/// </summary>
[Required]
public long TotalBefore { get; set; }
/// <summary>
/// Gets or sets the total number of items after this subset.
/// </summary>
[Required]
public long TotalAfter { get; set; }
/// <summary>
/// Gets or sets the items in the subset.
/// </summary>
[Required]
public IEnumerable<T> Items { get; set; } = Enumerable.Empty<T>();
/// <summary>
/// Creates an empty subset view model.
/// </summary>
/// <returns>An empty <see cref="SubsetViewModel{T}"/> instance.</returns>
public static SubsetViewModel<T> Empty() => new();
}
@@ -7,23 +7,6 @@ using Umbraco.Cms.Core.Hosting;
namespace Umbraco.Cms.Api.Management;
[BindProperties]
public class BackOfficeLoginModel
{
/// <summary>
/// Gets or sets the value of the "ReturnUrl" query parameter or defaults to the configured Umbraco directory.
/// </summary>
[FromQuery(Name = "ReturnUrl")]
public string? ReturnUrl { get; set; }
/// <summary>
/// The configured Umbraco directory.
/// </summary>
public string? UmbracoUrl { get; set; }
public bool UserIsAlreadyLoggedIn { get; set; }
}
[ApiExplorerSettings(IgnoreApi = true)]
[Route(LoginPath)]
public class BackOfficeLoginController : Controller
@@ -0,0 +1,20 @@
using Microsoft.AspNetCore.Mvc;
namespace Umbraco.Cms.Api.Management;
[BindProperties]
public class BackOfficeLoginModel
{
/// <summary>
/// Gets or sets the value of the "ReturnUrl" query parameter or defaults to the configured Umbraco directory.
/// </summary>
[FromQuery(Name = "ReturnUrl")]
public string? ReturnUrl { get; set; }
/// <summary>
/// The configured Umbraco directory.
/// </summary>
public string? UmbracoUrl { get; set; }
public bool UserIsAlreadyLoggedIn { get; set; }
}
@@ -43,8 +43,8 @@ public class DataTypeTreeControllerBase : FolderTreeControllerBase<DataTypeTreeI
{
get
{
var ordering = Ordering.By(nameof(Infrastructure.Persistence.Dtos.NodeDto.NodeObjectType), Direction.Descending); // We need to override to change direction
ordering.Next = Ordering.By(nameof(Infrastructure.Persistence.Dtos.NodeDto.Text));
var ordering = Ordering.By(Infrastructure.Persistence.Dtos.NodeDto.NodeObjectTypeColumnName, Direction.Descending); // We need to override to change direction
ordering.Next = Ordering.By(Infrastructure.Persistence.Dtos.NodeDto.TextColumnName);
return ordering;
}
@@ -5,6 +5,7 @@ using Microsoft.Extensions.DependencyInjection;
using Umbraco.Cms.Api.Management.Factories;
using Umbraco.Cms.Api.Management.Services.Entities;
using Umbraco.Cms.Api.Management.Services.Flags;
using Umbraco.Cms.Api.Management.Services.PermissionFilter;
using Umbraco.Cms.Api.Management.ViewModels.Tree;
using Umbraco.Cms.Core.Cache;
using Umbraco.Cms.Core.Security;
@@ -35,7 +36,7 @@ public class AncestorsDocumentTreeController : DocumentTreeControllerBase
{
}
[ActivatorUtilitiesConstructor]
[Obsolete("Please use the constructor taking all parameters. Scheduled for removal in Umbraco 19.")]
public AncestorsDocumentTreeController(
IEntityService entityService,
FlagProviderCollection flagProviders,
@@ -57,6 +58,30 @@ public class AncestorsDocumentTreeController : DocumentTreeControllerBase
{
}
[ActivatorUtilitiesConstructor]
public AncestorsDocumentTreeController(
IEntityService entityService,
FlagProviderCollection flagProviders,
IUserStartNodeEntitiesService userStartNodeEntitiesService,
IDataTypeService dataTypeService,
IPublicAccessService publicAccessService,
AppCaches appCaches,
IBackOfficeSecurityAccessor backofficeSecurityAccessor,
IDocumentPresentationFactory documentPresentationFactory,
IDocumentPermissionFilterService documentPermissionFilterService)
: base(
entityService,
flagProviders,
userStartNodeEntitiesService,
dataTypeService,
publicAccessService,
appCaches,
backofficeSecurityAccessor,
documentPresentationFactory,
documentPermissionFilterService)
{
}
[HttpGet("ancestors")]
[MapToApiVersion("1.0")]
[ProducesResponseType(typeof(IEnumerable<DocumentTreeItemResponseModel>), StatusCodes.Status200OK)]
@@ -6,6 +6,7 @@ using Umbraco.Cms.Api.Common.ViewModels.Pagination;
using Umbraco.Cms.Api.Management.Factories;
using Umbraco.Cms.Api.Management.Services.Entities;
using Umbraco.Cms.Api.Management.Services.Flags;
using Umbraco.Cms.Api.Management.Services.PermissionFilter;
using Umbraco.Cms.Api.Management.ViewModels.Tree;
using Umbraco.Cms.Core.Cache;
using Umbraco.Cms.Core.Security;
@@ -36,7 +37,7 @@ public class ChildrenDocumentTreeController : DocumentTreeControllerBase
{
}
[ActivatorUtilitiesConstructor]
[Obsolete("Please use the constructor taking all parameters. Scheduled for removal in Umbraco 19.")]
public ChildrenDocumentTreeController(
IEntityService entityService,
FlagProviderCollection flagProviders,
@@ -58,6 +59,30 @@ public class ChildrenDocumentTreeController : DocumentTreeControllerBase
{
}
[ActivatorUtilitiesConstructor]
public ChildrenDocumentTreeController(
IEntityService entityService,
FlagProviderCollection flagProviders,
IUserStartNodeEntitiesService userStartNodeEntitiesService,
IDataTypeService dataTypeService,
IPublicAccessService publicAccessService,
AppCaches appCaches,
IBackOfficeSecurityAccessor backofficeSecurityAccessor,
IDocumentPresentationFactory documentPresentationFactory,
IDocumentPermissionFilterService documentPermissionFilterService)
: base(
entityService,
flagProviders,
userStartNodeEntitiesService,
dataTypeService,
publicAccessService,
appCaches,
backofficeSecurityAccessor,
documentPresentationFactory,
documentPermissionFilterService)
{
}
[HttpGet("children")]
[MapToApiVersion("1.0")]
[ProducesResponseType(typeof(PagedViewModel<DocumentTreeItemResponseModel>), StatusCodes.Status200OK)]
@@ -6,6 +6,7 @@ using Umbraco.Cms.Api.Management.Factories;
using Umbraco.Cms.Api.Management.Routing;
using Umbraco.Cms.Api.Management.Services.Entities;
using Umbraco.Cms.Api.Management.Services.Flags;
using Umbraco.Cms.Api.Management.Services.PermissionFilter;
using Umbraco.Cms.Api.Management.ViewModels;
using Umbraco.Cms.Api.Management.ViewModels.Tree;
using Umbraco.Cms.Core;
@@ -28,6 +29,7 @@ public abstract class DocumentTreeControllerBase : UserStartNodeTreeControllerBa
private readonly AppCaches _appCaches;
private readonly IBackOfficeSecurityAccessor _backofficeSecurityAccessor;
private readonly IDocumentPresentationFactory _documentPresentationFactory;
private readonly IDocumentPermissionFilterService _documentPermissionFilterService;
[Obsolete("Please use the constructor taking all parameters. Scheduled for removal in Umbraco 18.")]
protected DocumentTreeControllerBase(
@@ -50,7 +52,7 @@ public abstract class DocumentTreeControllerBase : UserStartNodeTreeControllerBa
{
}
[ActivatorUtilitiesConstructor]
[Obsolete("Please use the constructor taking all parameters. Scheduled for removal in Umbraco 19.")]
protected DocumentTreeControllerBase(
IEntityService entityService,
FlagProviderCollection flagProviders,
@@ -60,17 +62,42 @@ public abstract class DocumentTreeControllerBase : UserStartNodeTreeControllerBa
AppCaches appCaches,
IBackOfficeSecurityAccessor backofficeSecurityAccessor,
IDocumentPresentationFactory documentPresentationFactory)
: this(
entityService,
flagProviders,
userStartNodeEntitiesService,
dataTypeService,
publicAccessService,
appCaches,
backofficeSecurityAccessor,
documentPresentationFactory,
StaticServiceProvider.Instance.GetRequiredService<IDocumentPermissionFilterService>())
{
}
[ActivatorUtilitiesConstructor]
protected DocumentTreeControllerBase(
IEntityService entityService,
FlagProviderCollection flagProviders,
IUserStartNodeEntitiesService userStartNodeEntitiesService,
IDataTypeService dataTypeService,
IPublicAccessService publicAccessService,
AppCaches appCaches,
IBackOfficeSecurityAccessor backofficeSecurityAccessor,
IDocumentPresentationFactory documentPresentationFactory,
IDocumentPermissionFilterService documentPermissionFilterService)
: base(entityService, flagProviders, userStartNodeEntitiesService, dataTypeService)
{
_publicAccessService = publicAccessService;
_appCaches = appCaches;
_backofficeSecurityAccessor = backofficeSecurityAccessor;
_documentPresentationFactory = documentPresentationFactory;
_documentPermissionFilterService = documentPermissionFilterService;
}
protected override UmbracoObjectTypes ItemObjectType => UmbracoObjectTypes.Document;
protected override Ordering ItemOrdering => Ordering.By(nameof(Infrastructure.Persistence.Dtos.NodeDto.SortOrder));
protected override Ordering ItemOrdering => Ordering.By(Infrastructure.Persistence.Dtos.NodeDto.SortOrderColumnName);
protected override DocumentTreeItemResponseModel MapTreeItemViewModel(Guid? parentId, IEntitySlim entity)
{
@@ -92,6 +119,7 @@ public abstract class DocumentTreeControllerBase : UserStartNodeTreeControllerBa
return responseModel;
}
/// <inheritdoc/>
protected override int[] GetUserStartNodeIds()
=> _backofficeSecurityAccessor
.BackOfficeSecurity?
@@ -99,10 +127,19 @@ public abstract class DocumentTreeControllerBase : UserStartNodeTreeControllerBa
.CalculateContentStartNodeIds(EntityService, _appCaches)
?? Array.Empty<int>();
/// <inheritdoc/>
protected override string[] GetUserStartNodePaths()
=> _backofficeSecurityAccessor
.BackOfficeSecurity?
.CurrentUser?
.GetContentStartNodePaths(EntityService, _appCaches)
?? Array.Empty<string>();
/// <inheritdoc/>
protected override Task<(IEntitySlim[] Entities, long TotalItems)> FilterTreeEntities(IEntitySlim[] entities, long totalItems)
=> _documentPermissionFilterService.FilterAsync(entities, totalItems);
/// <inheritdoc/>
protected override Task<(IEntitySlim[] Entities, long TotalBefore, long TotalAfter)> FilterTreeEntities(Guid targetKey, IEntitySlim[] entities, long totalBefore, long totalAfter)
=> _documentPermissionFilterService.FilterAsync(targetKey, entities, totalBefore, totalAfter);
}
@@ -6,6 +6,7 @@ using Umbraco.Cms.Api.Common.ViewModels.Pagination;
using Umbraco.Cms.Api.Management.Factories;
using Umbraco.Cms.Api.Management.Services.Entities;
using Umbraco.Cms.Api.Management.Services.Flags;
using Umbraco.Cms.Api.Management.Services.PermissionFilter;
using Umbraco.Cms.Api.Management.ViewModels.Tree;
using Umbraco.Cms.Core.Cache;
using Umbraco.Cms.Core.Security;
@@ -36,7 +37,7 @@ public class RootDocumentTreeController : DocumentTreeControllerBase
{
}
[ActivatorUtilitiesConstructor]
[Obsolete("Please use the constructor taking all parameters. Scheduled for removal in Umbraco 19.")]
public RootDocumentTreeController(
IEntityService entityService,
FlagProviderCollection flagProviders,
@@ -58,6 +59,30 @@ public class RootDocumentTreeController : DocumentTreeControllerBase
{
}
[ActivatorUtilitiesConstructor]
public RootDocumentTreeController(
IEntityService entityService,
FlagProviderCollection flagProviders,
IUserStartNodeEntitiesService userStartNodeEntitiesService,
IDataTypeService dataTypeService,
IPublicAccessService publicAccessService,
AppCaches appCaches,
IBackOfficeSecurityAccessor backofficeSecurityAccessor,
IDocumentPresentationFactory documentPresentationFactory,
IDocumentPermissionFilterService documentPermissionFilterService)
: base(
entityService,
flagProviders,
userStartNodeEntitiesService,
dataTypeService,
publicAccessService,
appCaches,
backofficeSecurityAccessor,
documentPresentationFactory,
documentPermissionFilterService)
{
}
[HttpGet("root")]
[MapToApiVersion("1.0")]
[ProducesResponseType(typeof(PagedViewModel<DocumentTreeItemResponseModel>), StatusCodes.Status200OK)]
@@ -6,6 +6,7 @@ using Umbraco.Cms.Api.Common.ViewModels.Pagination;
using Umbraco.Cms.Api.Management.Factories;
using Umbraco.Cms.Api.Management.Services.Entities;
using Umbraco.Cms.Api.Management.Services.Flags;
using Umbraco.Cms.Api.Management.Services.PermissionFilter;
using Umbraco.Cms.Api.Management.ViewModels.Tree;
using Umbraco.Cms.Core.Cache;
using Umbraco.Cms.Core.Security;
@@ -36,7 +37,7 @@ public class SiblingsDocumentTreeController : DocumentTreeControllerBase
{
}
[ActivatorUtilitiesConstructor]
[Obsolete("Please use the constructor taking all parameters. Scheduled for removal in Umbraco 19.")]
public SiblingsDocumentTreeController(
IEntityService entityService,
FlagProviderCollection flagProviders,
@@ -58,6 +59,30 @@ public class SiblingsDocumentTreeController : DocumentTreeControllerBase
{
}
[ActivatorUtilitiesConstructor]
public SiblingsDocumentTreeController(
IEntityService entityService,
FlagProviderCollection flagProviders,
IUserStartNodeEntitiesService userStartNodeEntitiesService,
IDataTypeService dataTypeService,
IPublicAccessService publicAccessService,
AppCaches appCaches,
IBackOfficeSecurityAccessor backofficeSecurityAccessor,
IDocumentPresentationFactory documentPresentationFactory,
IDocumentPermissionFilterService documentPermissionFilterService)
: base(
entityService,
flagProviders,
userStartNodeEntitiesService,
dataTypeService,
publicAccessService,
appCaches,
backofficeSecurityAccessor,
documentPresentationFactory,
documentPermissionFilterService)
{
}
[HttpGet("siblings")]
[MapToApiVersion("1.0")]
[ProducesResponseType(typeof(SubsetViewModel<DocumentTreeItemResponseModel>), StatusCodes.Status200OK)]
@@ -43,8 +43,8 @@ public class DocumentBlueprintTreeControllerBase : FolderTreeControllerBase<Docu
{
get
{
var ordering = Ordering.By(nameof(Infrastructure.Persistence.Dtos.NodeDto.NodeObjectType), Direction.Descending); // We need to override to change direction
ordering.Next = Ordering.By(nameof(Infrastructure.Persistence.Dtos.NodeDto.Text));
var ordering = Ordering.By(Infrastructure.Persistence.Dtos.NodeDto.NodeObjectTypeColumnName, Direction.Descending); // We need to override to change direction
ordering.Next = Ordering.By(Infrastructure.Persistence.Dtos.NodeDto.TextColumnName);
return ordering;
}
@@ -56,7 +56,7 @@ public abstract class ManagementApiControllerBase : Controller, IUmbracoFeature
/// Creates a 403 Forbidden result.
/// </summary>
/// <remarks>
/// Use this method instead of <see cref="ManagementApiControllerBase.Forbid()"/> on the controller base.
/// Use this method instead of the controller base class's Forbid() method.
/// This method ensures that a proper 403 Forbidden status code is returned to the client.
/// </remarks>
// Duplicate code copied between Management API and Delivery API.
@@ -64,7 +64,7 @@ public class MediaTreeControllerBase : UserStartNodeTreeControllerBase<MediaTree
protected override UmbracoObjectTypes ItemObjectType => UmbracoObjectTypes.Media;
protected override Ordering ItemOrdering => Ordering.By(nameof(Infrastructure.Persistence.Dtos.NodeDto.SortOrder));
protected override Ordering ItemOrdering => Ordering.By(Infrastructure.Persistence.Dtos.NodeDto.SortOrderColumnName);
protected override MediaTreeItemResponseModel MapTreeItemViewModel(Guid? parentKey, IEntitySlim entity)
{
@@ -168,7 +168,7 @@ public abstract class RecycleBinControllerBase<TItem> : ContentControllerBase
after,
out totalBefore,
out totalAfter,
ordering: Ordering.By(nameof(Infrastructure.Persistence.Dtos.NodeDto.Text)))
ordering: Ordering.By(Infrastructure.Persistence.Dtos.NodeDto.TextColumnName))
.ToArray();
/// <summary>
@@ -36,12 +36,14 @@ public abstract class EntityTreeControllerBase<TItem> : ManagementApiControllerB
protected abstract UmbracoObjectTypes ItemObjectType { get; }
protected virtual Ordering ItemOrdering => Ordering.By(nameof(Infrastructure.Persistence.Dtos.NodeDto.Text));
protected virtual Ordering ItemOrdering => Ordering.By(Infrastructure.Persistence.Dtos.NodeDto.TextColumnName);
protected async Task<ActionResult<PagedViewModel<TItem>>> GetRoot(int skip, int take)
{
IEntitySlim[] rootEntities = GetPagedRootEntities(skip, take, out var totalItems);
(rootEntities, totalItems) = await FilterTreeEntities(rootEntities, totalItems);
TItem[] treeItemViewModels = MapTreeItemViewModels(null, rootEntities);
await PopulateFlags(treeItemViewModels);
@@ -55,6 +57,8 @@ public abstract class EntityTreeControllerBase<TItem> : ManagementApiControllerB
{
IEntitySlim[] children = GetPagedChildEntities(parentId, skip, take, out var totalItems);
(children, totalItems) = await FilterTreeEntities(children, totalItems);
TItem[] treeItemViewModels = MapTreeItemViewModels(parentId, children);
await PopulateFlags(treeItemViewModels);
@@ -72,6 +76,8 @@ public abstract class EntityTreeControllerBase<TItem> : ManagementApiControllerB
return NotFound();
}
(siblings, totalBefore, totalAfter) = await FilterTreeEntities(target, siblings, totalBefore, totalAfter);
IEntitySlim? entity = siblings.FirstOrDefault();
Guid? parentKey = GetParentKey(entity);
@@ -84,6 +90,34 @@ public abstract class EntityTreeControllerBase<TItem> : ManagementApiControllerB
return Ok(result);
}
/// <summary>
/// Filters the specified collection of tree entities and returns the filtered results asynchronously.
/// </summary>
/// <param name="entities">An array of entities to be filtered.</param>
/// <param name="totalItems">The total number of items before filtering.</param>
/// <returns>A task that represents the asynchronous operation. The task result contains a tuple of the filtered entities and the adjusted total items count.</returns>
/// <remarks>
/// Override this method to implement custom filtering logic for tree entities. The default
/// implementation returns the input array and total items unchanged.
/// </remarks>
protected virtual Task<(IEntitySlim[] Entities, long TotalItems)> FilterTreeEntities(IEntitySlim[] entities, long totalItems)
=> Task.FromResult((entities, totalItems));
/// <summary>
/// Filters the specified collection of tree entities for sibling queries and returns the filtered results asynchronously.
/// </summary>
/// <param name="targetKey">The key of the target entity around which siblings are being retrieved.</param>
/// <param name="entities">An array of entities to be filtered.</param>
/// <param name="totalBefore">The total number of siblings before the target entity.</param>
/// <param name="totalAfter">The total number of siblings after the target entity.</param>
/// <returns>A task that represents the asynchronous operation. The task result contains a tuple of the filtered entities and the adjusted before/after counts.</returns>
/// <remarks>
/// Override this method to implement custom filtering logic for sibling tree entities. The default
/// implementation returns the input array and totals unchanged.
/// </remarks>
protected virtual Task<(IEntitySlim[] Entities, long TotalBefore, long TotalAfter)> FilterTreeEntities(Guid targetKey, IEntitySlim[] entities, long totalBefore, long totalAfter)
=> Task.FromResult((entities, totalBefore, totalAfter));
/// <summary>
/// Gets the parent key for an entity, or root if null or no parent.
/// </summary>
@@ -24,8 +24,8 @@ public abstract class FolderTreeControllerBase<TItem> : NamedEntityTreeControlle
get
{
// Override to order by type (folder vs item) before the text
var ordering = Ordering.By(nameof(Infrastructure.Persistence.Dtos.NodeDto.NodeObjectType));
ordering.Next = Ordering.By(nameof(Infrastructure.Persistence.Dtos.NodeDto.Text));
var ordering = Ordering.By(Infrastructure.Persistence.Dtos.NodeDto.NodeObjectTypeColumnName);
ordering.Next = Ordering.By(Infrastructure.Persistence.Dtos.NodeDto.TextColumnName);
return ordering;
}
@@ -1,6 +1,7 @@
using Microsoft.Extensions.DependencyInjection;
using Umbraco.Cms.Api.Management.Factories;
using Umbraco.Cms.Api.Management.Mapping.Document;
using Umbraco.Cms.Api.Management.Services.PermissionFilter;
using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Core.Mapping;
@@ -10,6 +11,7 @@ internal static class DocumentBuilderExtensions
{
internal static IUmbracoBuilder AddDocuments(this IUmbracoBuilder builder)
{
builder.Services.AddScoped<IDocumentPermissionFilterService, DocumentPermissionFilterService>();
builder.Services.AddTransient<IDocumentPresentationFactory, DocumentPresentationFactory>();
builder.Services.AddTransient<IDocumentNotificationPresentationFactory, DocumentNotificationPresentationFactory>();
builder.Services.AddTransient<IDocumentUrlFactory, DocumentUrlFactory>();
@@ -1,4 +1,4 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection;
using Umbraco.Cms.Api.Management.Factories;
using Umbraco.Cms.Api.Management.Mapping.TemporaryFile;
using Umbraco.Cms.Core.DependencyInjection;
@@ -6,7 +6,7 @@ using Umbraco.Cms.Core.Mapping;
namespace Umbraco.Cms.Api.Management.DependencyInjection;
internal static class TTemporaryFileBuilderExtensions
internal static class TemporaryFileBuilderExtensions
{
internal static IUmbracoBuilder AddTemporaryFiles(this IUmbracoBuilder builder)
{
@@ -32,20 +32,15 @@ public class MediaUrlFactory : IMediaUrlFactory
.Select(mediaUrl => new MediaUrlInfo
{
Culture = null,
Url = CreateMediaUrl(mediaUrl),
Url = CreateMediaUrl(mediaUrl, media.Trashed),
})
.ToArray();
private string CreateMediaUrl(string mediaUrl)
private string CreateMediaUrl(string mediaUrl, bool isTrashed)
{
var url = _absoluteUrlBuilder.ToAbsoluteUrl(mediaUrl).ToString();
if (_contentSettings.EnableMediaRecycleBinProtection is false)
{
return url;
}
return _contentSettings.EnableMediaRecycleBinProtection
return isTrashed && _contentSettings.EnableMediaRecycleBinProtection
? AddProtectedSuffixToMediaUrl(url)
: url;
}
@@ -58,7 +58,8 @@ public class RelationTypePresentationFactory : IRelationTypePresentationFactory
return Task.FromResult<IEnumerable<IReferenceResponseModel>>(result);
}
private IReferenceResponseModel? MapDocumentReference(RelationItemModel relationItemModel,
private IReferenceResponseModel? MapDocumentReference(
RelationItemModel relationItemModel,
List<IEntitySlim> slimEntities)
{
DocumentReferenceResponseModel? documentReferenceResponseModel =
@@ -203,7 +203,7 @@ public class UserGroupPresentationFactory : IUserGroupPresentationFactory
current.Description = request.Description;
current.Icon = request.Icon;
current.HasAccessToAllLanguages = request.HasAccessToAllLanguages;
current.Permissions = request.FallbackPermissions;
current.GranularPermissions = await _permissionPresentationFactory.CreatePermissionSetsAsync(request.Permissions);
@@ -8,11 +8,11 @@ using Umbraco.Extensions;
namespace Umbraco.Cms.Api.Management.Middleware;
/// <summary>
/// Used to handle errors registered by external login providers
/// Used to handle errors registered by external login providers.
/// </summary>
/// <remarks>
/// When an external login provider registers an error with
/// <see cref="Extensions.HttpContextExtensions.SetExternalLoginProviderErrors" /> during the OAuth process,
/// <see cref="HttpContextExtensions.SetExternalLoginProviderErrors" /> during the OAuth process,
/// this middleware will detect that, store the errors into cookie data and redirect to the back office login so we can
/// read the errors back out.
/// </remarks>
@@ -38700,6 +38700,10 @@
"alias": {
"type": "string"
},
"description": {
"type": "string",
"nullable": true
},
"icon": {
"type": "string",
"nullable": true
@@ -46936,6 +46940,7 @@
"required": [
"allowLocalLogin",
"allowPasswordReset",
"umbracoCssPath",
"versionCheckPeriod"
],
"type": "object",
@@ -46949,6 +46954,9 @@
},
"allowLocalLogin": {
"type": "boolean"
},
"umbracoCssPath": {
"type": "string"
}
},
"additionalProperties": false
@@ -49177,6 +49185,10 @@
"alias": {
"type": "string"
},
"description": {
"type": "string",
"nullable": true
},
"icon": {
"type": "string",
"nullable": true
@@ -49614,6 +49626,10 @@
"alias": {
"type": "string"
},
"description": {
"type": "string",
"nullable": true
},
"icon": {
"type": "string",
"nullable": true
@@ -1,10 +1,15 @@
using System.Globalization;
using System.Collections.Immutable;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using OpenIddict.Abstractions;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Configuration.Models;
using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Infrastructure.Security;
@@ -14,10 +19,12 @@ public class BackOfficeApplicationManager : OpenIdDictApplicationManagerBase, IB
{
private readonly IWebHostEnvironment _webHostEnvironment;
private readonly IRuntimeState _runtimeState;
private readonly ILogger<BackOfficeApplicationManager> _logger;
private readonly Uri? _backOfficeHost;
private readonly string _authorizeCallbackPathName;
private readonly string _authorizeCallbackLogoutPathName;
[Obsolete("Use the non obsoleted constructor instead. Scheduled for removal in v19")]
public BackOfficeApplicationManager(
IOpenIddictApplicationManager applicationManager,
IWebHostEnvironment webHostEnvironment,
@@ -30,12 +37,29 @@ public class BackOfficeApplicationManager : OpenIdDictApplicationManagerBase, IB
_backOfficeHost = securitySettings.Value.BackOfficeHost;
_authorizeCallbackPathName = securitySettings.Value.AuthorizeCallbackPathName;
_authorizeCallbackLogoutPathName = securitySettings.Value.AuthorizeCallbackLogoutPathName;
_logger = StaticServiceProvider.Instance.GetRequiredService<ILogger<BackOfficeApplicationManager>>();
}
public BackOfficeApplicationManager(
IOpenIddictApplicationManager applicationManager,
IWebHostEnvironment webHostEnvironment,
IOptions<SecuritySettings> securitySettings,
IRuntimeState runtimeState,
ILogger<BackOfficeApplicationManager> logger)
: base(applicationManager)
{
_webHostEnvironment = webHostEnvironment;
_runtimeState = runtimeState;
_logger = logger;
_backOfficeHost = securitySettings.Value.BackOfficeHost;
_authorizeCallbackPathName = securitySettings.Value.AuthorizeCallbackPathName;
_authorizeCallbackLogoutPathName = securitySettings.Value.AuthorizeCallbackLogoutPathName;
}
public async Task EnsureBackOfficeApplicationAsync(IEnumerable<Uri> backOfficeHosts, CancellationToken cancellationToken = default)
{
// Install is okay without this, because we do not need a token to install,
// but upgrades do, so we need to execute for everything higher then or equal to upgrade.
// but upgrades do, so we need to execute for everything higher than or equal to upgrade.
if (_runtimeState.Level < RuntimeLevel.Upgrade)
{
return;
@@ -47,6 +71,18 @@ public class BackOfficeApplicationManager : OpenIdDictApplicationManagerBase, IB
throw new ArgumentException($"Expected absolute URLs, got: {string.Join(", ", backOfficeHostsAsArray.Select(url => url.ToString()))}", nameof(backOfficeHosts));
}
// A balanced environment:
// - has 2 or more dedicated CD (Content Delivery) servers - ServerRole.Subscriber
// - has 1 or more dedicated CM (Content Management) servers - ServerRole.SchedulingPublisher
// The CD and CM URLs are different for the backoffice and the individual servers do not start at the same time, for example:
// CD www.domain.com/umbraco
// CM cms.domain.com/umbraco
// To access the Umbraco Backoffice, it is absolutely necessary to add the address of each server to the OpenId, as they share one database!
// Destination table: umbracoOpenIddictApplications
// Destination Fields: RedirectUris and PostLogoutRedirectUris
// Read saved settings from DB and add unique additional servers.
backOfficeHostsAsArray = await MergeWithExistingBackOfficeHostsAsync(backOfficeHostsAsArray, cancellationToken);
await CreateOrUpdate(
BackofficeOpenIddictApplicationDescriptor(backOfficeHostsAsArray),
cancellationToken);
@@ -96,6 +132,94 @@ public class BackOfficeApplicationManager : OpenIdDictApplicationManagerBase, IB
public async Task DeleteBackOfficeClientCredentialsApplicationAsync(string clientId, CancellationToken cancellationToken = default)
=> await Delete(clientId, cancellationToken);
/// <summary>
/// Merges new back-office hosts with existing hosts from the database.
/// Uses OpenIddict API to read existing redirect URIs and extracts unique authorities (hosts).
/// Handles invalid URIs gracefully by skipping them.
/// </summary>
/// <param name="newHosts">The new hosts to merge</param>
/// <param name="cancellationToken">Cancellation token</param>
/// <returns>Array of merged unique hosts (by authority, case-insensitive)</returns>
private async Task<Uri[]> MergeWithExistingBackOfficeHostsAsync(Uri[] newHosts, CancellationToken cancellationToken)
{
// Find an existing back-office application
var application = await ApplicationManager.FindByClientIdAsync(Constants.OAuthClientIds.BackOffice, cancellationToken);
if (application is null)
{
// No existing application, return new hosts as-is
return newHosts;
}
// Get existing redirect URIs using OpenIddict API
ImmutableArray<string> existingRedirectUris = await ApplicationManager.GetRedirectUrisAsync(application, cancellationToken);
// Use HashSet for O(n) performance and automatic deduplication
// Case-insensitive comparison for authorities (host names)
var mergedAuthorities = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
// Extract authorities from existing redirect URIs
foreach (var existingUriString in existingRedirectUris)
{
if (TryGetAuthorityFromUriString(existingUriString, out var authority))
{
mergedAuthorities.Add(authority);
}
}
// Add new hosts' authorities
foreach (Uri newHost in newHosts)
{
if (TryGetAuthorityFromUri(newHost, out var authority))
{
mergedAuthorities.Add(authority);
}
}
// Convert back to Uri array
return mergedAuthorities.Select(authority => new Uri(authority)).ToArray();
}
private bool TryGetAuthorityFromUri(Uri uri, [NotNullWhen(true)] out string? authority)
{
try
{
if (uri.IsAbsoluteUri)
{
authority = uri.GetLeftPart(UriPartial.Authority);
return true;
}
}
catch (InvalidOperationException)
{
// GetLeftPart can throw InvalidOperationException for some URI types
// Skip malformed URIs
_logger.LogDebug("Could not extract authority from URI {Uri} as the left part could not be identified, skipping", uri);
}
authority = null;
return false;
}
private bool TryGetAuthorityFromUriString(string uriString, [NotNullWhen(true)] out string? authority)
{
try
{
var existingUri = new Uri(uriString);
if (TryGetAuthorityFromUri(existingUri, out authority))
{
return true;
}
}
catch (UriFormatException)
{
// Skip URIs with invalid format
_logger.LogDebug("Could not extract authority from uriString {String} because of malformed uri format, skipping", uriString);
}
authority = null;
return false;
}
internal OpenIddictApplicationDescriptor BackofficeOpenIddictApplicationDescriptor(Uri backOfficeUrl)
=> BackofficeOpenIddictApplicationDescriptor([backOfficeUrl]);
@@ -1,6 +1,11 @@
using Microsoft.AspNetCore.SignalR;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Core.Models.ServerEvents;
using Umbraco.Cms.Core.ServerEvents;
using Umbraco.Cms.Core.Services;
namespace Umbraco.Cms.Api.Management.ServerEvents;
@@ -9,22 +14,59 @@ internal sealed class ServerEventRouter : IServerEventRouter
{
private readonly IHubContext<ServerEventHub, IServerEventHub> _eventHub;
private readonly IUserConnectionManager _connectionManager;
private readonly IRuntimeState _runtimeState;
private readonly ILogger<ServerEventRouter> _logger;
[Obsolete("Please use the constructor that takes all parameters. Scheduled for removal in Umbraco 18.")]
public ServerEventRouter(
IHubContext<ServerEventHub, IServerEventHub> eventHub,
IUserConnectionManager connectionManager)
: this(
eventHub,
connectionManager,
StaticServiceProvider.Instance.GetRequiredService<IRuntimeState>(),
StaticServiceProvider.Instance.GetRequiredService<ILogger<ServerEventRouter>>())
{
}
public ServerEventRouter(
IHubContext<ServerEventHub, IServerEventHub> eventHub,
IUserConnectionManager connectionManager,
IRuntimeState runtimeState,
ILogger<ServerEventRouter> logger)
{
_eventHub = eventHub;
_connectionManager = connectionManager;
_runtimeState = runtimeState;
_logger = logger;
}
/// <inheritdoc/>
public Task RouteEventAsync(ServerEvent serverEvent)
=> _eventHub.Clients.Group(serverEvent.EventSource).notify(serverEvent);
public async Task RouteEventAsync(ServerEvent serverEvent)
{
if (_runtimeState.Level != RuntimeLevel.Run)
{
return;
}
try
{
await _eventHub.Clients.Group(serverEvent.EventSource).notify(serverEvent);
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Failed to route server event {EventType} for {EventSource}", serverEvent.EventType, serverEvent.EventSource);
}
}
/// <inheritdoc/>
public async Task NotifyUserAsync(ServerEvent serverEvent, Guid userKey)
{
if (_runtimeState.Level != RuntimeLevel.Run)
{
return;
}
ISet<string> userConnections = _connectionManager.GetConnections(userKey);
if (userConnections.Any() is false)
@@ -32,10 +74,31 @@ internal sealed class ServerEventRouter : IServerEventRouter
return;
}
await _eventHub.Clients.Clients(userConnections).notify(serverEvent);
try
{
await _eventHub.Clients.Clients(userConnections).notify(serverEvent);
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Failed to notify user {UserKey} of server event {EventType}", userKey, serverEvent.EventType);
}
}
/// <inheritdoc/>
public async Task BroadcastEventAsync(ServerEvent serverEvent) => await _eventHub.Clients.All.notify(serverEvent);
public async Task BroadcastEventAsync(ServerEvent serverEvent)
{
if (_runtimeState.Level != RuntimeLevel.Run)
{
return;
}
try
{
await _eventHub.Clients.All.notify(serverEvent);
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Failed to broadcast server event {EventType}", serverEvent.EventType);
}
}
}
@@ -54,7 +54,7 @@ public interface IUserStartNodeEntitiesService
/// <summary>
/// Calculates the applicable child entities from a list of candidate child entities for users without root access.
/// </summary>
/// <param name="candidateChildren">The candidate child entities to filter (i.e. entities fetched with <see cref="EntityService.GetPagedChildren"/>).</param>
/// <param name="candidateChildren">The candidate child entities to filter (i.e. entities fetched with the EntityService's GetPagedChildren method).</param>
/// <param name="userStartNodePaths">The calculated start node paths for the user.</param>
/// <returns>A list of child entities applicable entities for the user.</returns>
/// <remarks>
@@ -142,7 +142,8 @@ public class UserStartNodeEntitiesService : IUserStartNodeEntitiesService
}
// is ancestor of a start node?
if (userStartNodePaths.Any(path => path.StartsWith(child.Path)))
// Note: Add trailing comma to prevent false matches (e.g., path "-1,100" should not match "-1,1001")
if (userStartNodePaths.Any(path => path.StartsWith($"{child.Path},")))
{
return new UserAccessEntity(child, false);
}
@@ -220,5 +221,7 @@ public class UserStartNodeEntitiesService : IUserStartNodeEntitiesService
=> entities.Select(entity => new UserAccessEntity(entity, IsDescendantOrSelf(entity, userStartNodePaths))).ToArray();
private static bool IsDescendantOrSelf(IEntitySlim child, string[] userStartNodePaths)
=> userStartNodePaths.Any(path => child.Path.StartsWith(path));
// Note: Add trailing commas to both paths to prevent false matches (e.g., path "-1,100" should not match "-1,1001")
// This matches the pattern used in lines 92 and 192 of this file
=> userStartNodePaths.Any(path => $"{child.Path},".StartsWith($"{path},"));
}
@@ -21,7 +21,7 @@ internal class HasScheduleFlagProvider : IFlagProvider
private readonly IIdKeyMap _keyMap;
/// <summary>
/// Initializes a new instance of the <see cref="HasScheduleSignProvider"/> class.
/// Initializes a new instance of the <see cref="HasScheduleFlagProvider"/> class.
/// </summary>
public HasScheduleFlagProvider(IContentService contentService, IIdKeyMap keyMap)
{
@@ -0,0 +1,91 @@
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Actions;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.Entities;
using Umbraco.Cms.Core.Security;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Services.OperationStatus;
namespace Umbraco.Cms.Api.Management.Services.PermissionFilter;
/// <summary>
/// Provides functionality to filter document entities based on the current user's permissions.
/// </summary>
internal sealed class DocumentPermissionFilterService : IDocumentPermissionFilterService
{
private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor;
private readonly IUserService _userService;
/// <summary>
/// Initializes a new instance of the <see cref="DocumentPermissionFilterService"/> class.
/// </summary>
/// <param name="backOfficeSecurityAccessor">Provides access to the current backoffice user's security context.</param>
/// <param name="userService">Service used to retrieve user and document permissions.</param>
public DocumentPermissionFilterService(
IBackOfficeSecurityAccessor backOfficeSecurityAccessor,
IUserService userService)
{
_backOfficeSecurityAccessor = backOfficeSecurityAccessor;
_userService = userService;
}
/// <inheritdoc />
public async Task<(IEntitySlim[] Entities, long TotalItems)> FilterAsync(IEntitySlim[] entities, long totalItems)
{
Dictionary<Guid, NodePermissions>? permissionsByNodeKey = await GetDocumentPermissionsByNodeKeyAsync(entities);
if (permissionsByNodeKey is null)
{
return (entities, totalItems);
}
IEntitySlim[] filteredEntities = FilterEntitiesWithBrowsePermission(entities, permissionsByNodeKey);
var removedCount = entities.Length - filteredEntities.Length;
return (filteredEntities, totalItems - removedCount);
}
/// <inheritdoc />
public async Task<(IEntitySlim[] Entities, long TotalBefore, long TotalAfter)> FilterAsync(Guid targetKey, IEntitySlim[] entities, long totalBefore, long totalAfter)
{
Dictionary<Guid, NodePermissions>? permissionsByNodeKey = await GetDocumentPermissionsByNodeKeyAsync(entities);
if (permissionsByNodeKey is null)
{
return (entities, totalBefore, totalAfter);
}
// Find the index of the target entity to determine before/after boundaries
var targetIndex = Array.FindIndex(entities, e => e.Key == targetKey);
// Count removed entities before and after the target separately
var removedBefore = entities.Take(targetIndex).Count(e => HasBrowsePermission(e, permissionsByNodeKey) is false);
var removedAfter = entities.Skip(targetIndex + 1).Count(e => HasBrowsePermission(e, permissionsByNodeKey) is false);
IEntitySlim[] filteredEntities = FilterEntitiesWithBrowsePermission(entities, permissionsByNodeKey);
return (filteredEntities, totalBefore - removedBefore, totalAfter - removedAfter);
}
private async Task<Dictionary<Guid, NodePermissions>?> GetDocumentPermissionsByNodeKeyAsync(IEntitySlim[] entities)
{
Guid userKey = CurrentUserKey();
var entityKeys = entities.Select(e => e.Key).ToHashSet();
Attempt<IEnumerable<NodePermissions>, UserOperationStatus> permissionsAttempt =
await _userService.GetDocumentPermissionsAsync(userKey, entityKeys);
return permissionsAttempt.Success
? permissionsAttempt.Result.ToDictionary(p => p.NodeKey)
: null;
}
private Guid CurrentUserKey()
=> _backOfficeSecurityAccessor.BackOfficeSecurity?.CurrentUser?.Key
?? throw new InvalidOperationException("No backoffice user found");
private static IEntitySlim[] FilterEntitiesWithBrowsePermission(IEntitySlim[] entities, Dictionary<Guid, NodePermissions> permissionsByNodeKey)
=> entities.Where(e => HasBrowsePermission(e, permissionsByNodeKey)).ToArray();
private static bool HasBrowsePermission(IEntitySlim entity, Dictionary<Guid, NodePermissions> permissionsByNodeKey)
=> permissionsByNodeKey.TryGetValue(entity.Key, out NodePermissions? nodePermissions) is false
|| nodePermissions.Permissions.Contains(ActionBrowse.ActionLetter);
}
@@ -0,0 +1,27 @@
using Umbraco.Cms.Core.Models.Entities;
namespace Umbraco.Cms.Api.Management.Services.PermissionFilter;
/// <summary>
/// Service for filtering document entities based on user browse permissions.
/// </summary>
public interface IDocumentPermissionFilterService
{
/// <summary>
/// Filters document entities based on the current user's browse permissions.
/// </summary>
/// <param name="entities">The entities to filter.</param>
/// <param name="totalItems">The total number of items before filtering.</param>
/// <returns>A tuple containing the filtered entities and the adjusted total items count.</returns>
Task<(IEntitySlim[] Entities, long TotalItems)> FilterAsync(IEntitySlim[] entities, long totalItems);
/// <summary>
/// Filters sibling document entities based on the current user's browse permissions.
/// </summary>
/// <param name="targetKey">The key of the target entity around which siblings are being retrieved.</param>
/// <param name="entities">The entities to filter.</param>
/// <param name="totalBefore">The total number of siblings before the target entity.</param>
/// <param name="totalAfter">The total number of siblings after the target entity.</param>
/// <returns>A tuple containing the filtered entities and the adjusted before/after counts.</returns>
Task<(IEntitySlim[] Entities, long TotalBefore, long TotalAfter)> FilterAsync(Guid targetKey, IEntitySlim[] entities, long totalBefore, long totalAfter);
}
@@ -15,12 +15,11 @@
[CS1998] remove async or make method synchronous
[CS8524] switch statement
[IDE0060] removed unused parameter
[SA1649] file name match type
[CS0419] ambiguous reference
[CS1573] param tag for all parameters
[CS1574] unresolveable cref
-->
<WarningsNotAsErrors>$(WarningsNotAsErrors),SA1117,SA1401,SA1134,CS0108,CS0618,CS9042,CS1998,CS8524,IDE0060,SA1649,CS0419,CS1573,CS1574</WarningsNotAsErrors>
<WarningsNotAsErrors>$(WarningsNotAsErrors),SA1117,SA1401,SA1134,CS0108,CS0618,CS9042,CS1998,CS8524,IDE0060,CS0419,CS1573,CS1574</WarningsNotAsErrors>
</PropertyGroup>
<ItemGroup>
@@ -1,4 +1,4 @@
using Umbraco.Cms.Api.Management.ViewModels.ContentType;
using Umbraco.Cms.Api.Management.ViewModels.ContentType;
namespace Umbraco.Cms.Api.Management.ViewModels.MediaType;
@@ -17,4 +17,9 @@ public class CreateMediaTypeRequestModel
/// Gets or sets the compositions for this media type.
/// </summary>
public IEnumerable<MediaTypeComposition> Compositions { get; set; } = Enumerable.Empty<MediaTypeComposition>();
// TODO (V18): This is already declared on the base type, so for the next major, when we can allow a binary breaking change, we should remove it from here.
#pragma warning disable CS0108 // Member hides inherited member; missing new keyword
public ReferenceByIdModel? Collection { get; set; }
#pragma warning restore CS0108 // Member hides inherited member; missing new keyword
}
@@ -145,7 +145,7 @@ internal sealed class CollectibleRuntimeViewCompiler : IViewCompiler
// release the lock first.
cacheEntryOptions = new MemoryCacheEntryOptions();
Debug.Assert(item.ExpirationTokens != null);
Debug.Assert(item.ExpirationTokens != null, "Expiration tokens should not be null.");
for (var i = 0; i < item.ExpirationTokens.Count; i++)
{
cacheEntryOptions.ExpirationTokens.Add(item.ExpirationTokens[i]);
@@ -159,7 +159,7 @@ internal sealed class CollectibleRuntimeViewCompiler : IViewCompiler
else
{
// If we can't compile, we should have already created the descriptor
Debug.Assert(item.Descriptor != null);
Debug.Assert(item.Descriptor != null, "Descriptor should not be null when compilation is not supported.");
taskSource.SetResult(item.Descriptor);
}
@@ -169,13 +169,13 @@ internal sealed class CollectibleRuntimeViewCompiler : IViewCompiler
// Now the lock has been released so we can do more expensive processing.
if (item.SupportsCompilation)
{
Debug.Assert(taskSource != null);
Debug.Assert(taskSource != null, "Task source should not be null when compilation is supported.");
if (item.Descriptor?.Item != null &&
ChecksumValidator.IsItemValid(_projectEngine.FileSystem, item.Descriptor.Item))
{
// If the item has checksums to validate, we should also have a precompiled view.
Debug.Assert(item.Descriptor != null);
Debug.Assert(item.Descriptor != null, "Descriptor should not be null when checksums are valid.");
taskSource.SetResult(item.Descriptor);
return taskSource.Task;
@@ -426,7 +426,7 @@ internal sealed class CollectibleRuntimeViewCompiler : IViewCompiler
private string GetNormalizedPath(string relativePath)
{
Debug.Assert(relativePath != null);
Debug.Assert(relativePath != null, "Relative path should not be null.");
if (relativePath.Length == 0)
{
return relativePath;
@@ -1,4 +1,4 @@
using System.Diagnostics;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Reflection;
using Microsoft.AspNetCore.Hosting;
@@ -77,9 +77,9 @@ internal class CompilationOptionsProvider
_optionsInitialized = true;
}
Debug.Assert(_parseOptions is not null);
Debug.Assert(_compilationOptions is not null);
Debug.Assert(_emitOptions is not null);
Debug.Assert(_parseOptions is not null, "Parse options should be initialized.");
Debug.Assert(_compilationOptions is not null, "Compilation options should be initialized.");
Debug.Assert(_emitOptions is not null, "Emit options should be initialized.");
}
private DependencyContextCompilationOptions GetDependencyContextCompilationOptions()
@@ -4,7 +4,7 @@
<Description>Adds imaging support using ImageSharp/ImageSharp.Web version 2 to Umbraco CMS.</Description>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="SixLabors.ImageSharp" VersionOverride="[2.1.11, 3)" />
<PackageReference Include="SixLabors.ImageSharp" VersionOverride="[2.1.13, 3)" />
<PackageReference Include="SixLabors.ImageSharp.Web" VersionOverride="[2.0.2, 3)" />
</ItemGroup>
@@ -5,8 +5,12 @@ using Umbraco.Cms.Persistence.EFCore.Migrations;
namespace Umbraco.Cms.Persistence.EFCore.SqlServer;
/// <summary>
/// Composer for registering SQL Server EF Core migration services.
/// </summary>
public class EFCoreSqlServerComposer : IComposer
{
/// <inheritdoc />
public void Compose(IUmbracoBuilder builder)
{
builder.Services.AddSingleton<IMigrationProvider, SqlServerMigrationProvider>();
@@ -4,20 +4,30 @@ using Umbraco.Extensions;
namespace Umbraco.Cms.Persistence.EFCore.SqlServer;
/// <summary>
/// Provides SQL Server-specific migration functionality for Umbraco's EF Core database context.
/// </summary>
public class SqlServerMigrationProvider : IMigrationProvider
{
private readonly IDbContextFactory<UmbracoDbContext> _dbContextFactory;
/// <summary>
/// Initializes a new instance of the <see cref="SqlServerMigrationProvider"/> class.
/// </summary>
/// <param name="dbContextFactory">The factory for creating database context instances.</param>
public SqlServerMigrationProvider(IDbContextFactory<UmbracoDbContext> dbContextFactory) => _dbContextFactory = dbContextFactory;
/// <inheritdoc />
public string ProviderName => Constants.ProviderNames.SQLServer;
/// <inheritdoc />
public async Task MigrateAsync(EFCoreMigration migration)
{
UmbracoDbContext context = await _dbContextFactory.CreateDbContextAsync();
await context.MigrateDatabaseAsync(GetMigrationType(migration));
}
/// <inheritdoc />
public async Task MigrateAllAsync()
{
UmbracoDbContext context = await _dbContextFactory.CreateDbContextAsync();
@@ -4,10 +4,15 @@ using Umbraco.Cms.Persistence.EFCore.Migrations;
namespace Umbraco.Cms.Persistence.EFCore.SqlServer;
/// <summary>
/// Configures the EF Core DbContext to use SQL Server as the database provider.
/// </summary>
public class SqlServerMigrationProviderSetup : IMigrationProviderSetup
{
/// <inheritdoc />
public string ProviderName => Constants.ProviderNames.SQLServer;
/// <inheritdoc />
public void Setup(DbContextOptionsBuilder builder, string? connectionString)
{
builder.UseSqlServer(connectionString, x => x.MigrationsAssembly(GetType().Assembly.FullName));

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