Compare commits

...
Author SHA1 Message Date
Jacob Overgaard 4eae48e794 bump version to 16.2.0 2025-09-03 14:23:09 +02:00
Jacob Overgaard d32348d6b2 bump to rc3 2025-08-29 07:57:39 +02:00
Jacob OvergaardandGitHub e142a9c0d1 fix: pins the UmbracoExtension @hey-api/* versions to that of the Backoffice client (#20016)
fix: pins the @hey-api/* versions to that of the Backoffice client

This is a quick fix to handle the NPM error that is currently there because the Backoffice NPM client has moved on to another version. There will be a more comprehensive fix for 16.3, however this PR aims to make the 16.2 UmbracoExtension usable without running custom commands.
2025-08-28 09:01:44 +01:00
Lee KelleherandGitHub cef7bb9357 Chore: JSDocs for RTE CSS Props (#20008)
RTE base: added JSDocs for CSSProps
2025-08-27 13:12:26 +02:00
Jacob OvergaardandGitHub d895032e22 build(deps): bump @umbraco-ui/uui to 1.15.0 (#19981) 2025-08-26 10:11:07 +02:00
Jacob Overgaard 5a22e50120 Revert "build(deps): bump @umbraco-ui/uui to 1.15.0"
This reverts commit 9cb5a12143.
2025-08-25 08:57:29 +02:00
Jacob Overgaard 9cb5a12143 build(deps): bump @umbraco-ui/uui to 1.15.0 2025-08-25 08:55:56 +02:00
Jacob Overgaard e60acfe437 bump version to 16.2.0-rc2 2025-08-21 10:35:37 +02:00
CopilotJacob Overgaardcopilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
c436c4288b Fix TipTap word count extension state sharing between multiple editors (#19943)
* Initial plan

* Fix TipTap word count extension state sharing by using configure()

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>
2025-08-20 09:42:21 +02:00
Andy ButlandandZeegaan 229f1e0ce0 Fixed behaviour on database cache rebuild to update only for requested content types (#19905)
Fixed behaviour on database cache rebuild to update only for requested content types.

(cherry picked from commit b8b61cd326)
2025-08-14 09:44:13 +02:00
56569af0f9 Handle segmentation when segment alias includes underscore character(s) (#19782)
* move variant fragment split logic into splitview manager

* further centralise split logic into umbVariantId

* show segment selector if any exist

* invariant null

* chore: run eslint:fix

* chore(eslint): generate a UBM_ constant

---------

Co-authored-by: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com>
2025-08-13 16:28:43 +00:00
Jacob Overgaard fc3d8c8781 chore(eslint): adds eslint fixes 2025-08-13 18:05:26 +02:00
Jacob Overgaard 44af43e091 chore: run eslint:fix 2025-08-13 17:54:10 +02:00
Jacob Overgaard 94b3e767c7 set version back to 16.2 after merge 2025-08-13 17:23:52 +02:00
Jacob Overgaard db4a85fcb8 bump version to 16.3.0-rc 2025-08-13 16:53:51 +02:00
Mads RasmussenandGitHub 13bb25a8b8 Close dropdown when "Reload"-Entity Action has executed (#19808)
Bind 'action-executed' event handler to class instance

Updated the event listener for 'action-executed' to bind the handler to the class instance, ensuring correct 'this' context when the event is triggered.
2025-08-13 13:02:52 +00:00
Andy ButlandandGitHub 58cc7691a3 Adds configuration for document and media hybrid cache seed batch size (#19894)
Adds configuration for document and media hybrid cache seed batch size.
2025-08-13 10:12:16 +02:00
Jacob OvergaardandGitHub 937f4b8122 V16: Media Picker property editor does not handle dropped files appropriately (#19900)
* fix: make it clear that the clearUploads button is used to "Clear file(s)" and not necessarily remove them (from the dropzone)

* fix: adds extra null-check to avoid browser error on failed uploads

* fix: adds check that no media files are added twice (or more) to the media picker

* fix: adds try/catch around confirm modal to avoid browser error in case user cancels

* fix: change from deprecated 'complete' event to 'change' event and filter out non-successful files

* chore: sort imports

* feat: renders the 'add' button even if the limits have been exceeded

* feat: shows all values as cards even if the media item does not exist so the user has a chance to update the value

* feat: shows all values as cards even if the media item does not exist so the user has a chance to update the value

* feat: adds localization to the media picker context

* feat: uses the media picker context to control the picker

this also fixes an issue where already selected items were not preselected when opening the picker again

* feat: adds a bit of margin between the dropzone and media picker itself
2025-08-13 09:14:23 +02:00
cee441da49 Fix #19676 (#19886)
* observation as promise util

* all success observer

* next step todos

* await everything loaded

* contentTypeLoaded observable

* tidying up

* Apply suggestion from @Copilot

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

* remove comment

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-08-13 08:16:59 +02:00
aa269e317b Fix #19675 (#19891)
* clean up old stuff in validation form control mixin

* ensure validation trigger when value is changed

* Update src/Umbraco.Web.UI.Client/src/packages/property-editors/content-picker/property-editor-ui-content-picker.element.ts

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

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-08-13 08:16:17 +02:00
Andy ButlandandGitHub 2266529895 Added configuration option UseStrictDomainMatching, which allows control over whether content is routed without a matching domain (#19815)
* Added configuration option UseStrictDomainMatching, which allows control over whether content is routed without a matching domain.

* Fixed typo in comment.

* Addressed comments from code review.
2025-08-12 14:28:46 +01:00
Andy ButlandandGitHub 4efe8f59b8 Optimize document and media seeding by looking up from database in batches (#19890)
* Optimize document and media seeding by looking up from database in batches.

* Ensure null values aren't stored in the cache when checking existance.

* Fixed failing integration tests.

* Resolved issue with not writing to the L1 cache on an L2 hit.

* Tidied up and populated XML header comments.

* Address issue raised in code review.
2025-08-12 11:58:41 +02:00
417576b275 Content picker search with start node configured not taking user start nodes into account (#19871)
* Content picker search with start node configured not taking user start nodes into account (#19800)

* Fix users being able to see nodes they don't have access to when using the picker search

* Readability and naming improvements

* Additional fixes

* Adjust tests

* Additional fixes

* Small improvement

* Replaced the root ids with constants

* Update src/Umbraco.Web.BackOffice/Trees/MemberTreeController.cs

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

---------

Co-authored-by: Andy Butland <abutland73@gmail.com>
# Conflicts:
#	src/Umbraco.Examine.Lucene/BackOfficeExamineSearcher.cs
#	src/Umbraco.Web.BackOffice/Trees/ContentTreeController.cs
#	src/Umbraco.Web.BackOffice/Trees/MediaTreeController.cs
#	src/Umbraco.Web.BackOffice/Trees/MemberTreeController.cs
#	tests/Umbraco.Tests.Integration/Umbraco.Examine.Lucene/UmbracoExamine/BackOfficeExamineSearcherTests.cs

* Add new constructor without unused and obsolete parameters

* Use non obsolete constructor in tests

* Add `dataTypeId` as parameter in document and media search endpoints to get `ignoreUserStartNodes` value

* Update backend API generated typed client

* Updated picker search to pass in data type unique

* Move data type retrieval to UmbPickerContext

* Adjust the controller constructors to make it non breaking

* Adjust controller methods to make non-breaking.

---------

Co-authored-by: Andy Butland <abutland73@gmail.com>
2025-08-11 15:15:24 +02:00
Laura NetoandGitHub 2289493384 Moving properties between groups sometimes clears their values (#19881)
* Fix moving properties between groups sometimes clearing their values

* Small adjustment

* Fix failing integration test

The mapping method was only setting the property group when it was not null, but for orphaned properties we want to specifically set it to null.

* Adjust 'Can_Move_Properties_To_Another_Container' integration test to check more scenarios and that values are kept

* Adjust to add isElement variable in test (as previously)
2025-08-11 07:33:18 +02:00
2def046ea3 Eslint naming conventions (#19880)
* build(eslint): replace local rules with naming conventions

* revert relative js extension imports

* remove unused local rule

* build(eslint): uses recommended setup for import plugin

* chore(eslint): conver const to function to follow naming conventions

* chore: removes old file

* build(eslint): allows Ufm as prefix

* build(eslint): allows 'name' and 'extensions' as exports (umbraco-package.ts)

* build(eslint): typescript rules should ignore storybook

* chore(eslint): ignores eslint for vite definitions

* build(eslint): allows UPPER_CASE for properties

* build(eslint): ignores umbraco-package.ts files (unconventional exports)

* chore(storybook): fixes property editor stylesheet picker

* build(eslint): allows Manifest as prefix on interfaces

* build(eslint): allows underscore on protected members

* build(eslint): allows Meta as prefix on interfaces

* build(eslint): allows PascalCase for public members

* build(eslint): disables enforcement of booleans with verbs for now as it is too harsh

* chore(eslint): add private modifiers as required

* deprecates invalid constant name to replace with Umb prefix

* renames MediaValueType to comply with naming conventions

* chore(eslint): disable naming conventions for local router-slot package

* chore(eslint): follow naming conventions

* chore(eslint): disable naming conventions for property editor interfaces

* chore(eslint): follow naming conventions

* chore(storybook): fix story

* chore(eslint): follow naming conventions

* build(eslint): allows `_host` as public variable

* chore(eslint): follow naming conventions

* build(eslint): allows double leading underscore on public members

* build(eslint): matches #private and public modifiers

* build(eslint): ignores language files

* chore(eslint): ignores umbraco package file

* chore(eslint): follow naming conventions

* storybook lang

* chore(eslint): follow naming conventions

* chore(eslint): follow naming conventions

* chore(eslint): make _manager a little more open

* chore(eslint): some properties should be protected

* Update src/Umbraco.Web.UI.Client/src/packages/media/media/components/input-image-cropper/image-cropper.element.ts

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

* Update src/Umbraco.Web.UI.Client/src/packages/media/media/components/input-image-cropper/image-cropper.element.ts

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

* Update src/Umbraco.Web.UI.Client/src/packages/media/media/components/input-image-cropper/image-cropper.element.ts

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

* proxy type for UrlParametersRecord

* _items deprecated property

* bring back ConditionTypes type

* bring back _items for trash bulk action

* ignorer deprecated proxies

* keep settingsDataContentTypeKey for satefy

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Niels Lyngsø <niels.lyngso@gmail.com>
Co-authored-by: Niels Lyngsø <nsl@umbraco.dk>
2025-08-08 08:52:14 +00:00
Lee KelleherandGitHub b3d5744d34 Tiptap RTE: Reduce loading layout shift (#19860)
* Tiptap RTE: Set row/group min-height to prevent layout shift

* Added `box-sizing: border-box`

* Adds loaded state to the editor

so that the border only appears once it's ready.

* Refactored toolbar to reduce the number of re-renders

* Refactored statusbar to reduce the number of re-renders
2025-08-08 08:41:44 +02:00
Lee KelleherandGitHub 5f1ecbae94 Tiptap RTE: Codesweep for 16.2 (#19879)
* RTE mock data updates

* TODO comment typo correction

* Corrected typo in class name

This could technically be a breaking-change, but since the class name
conflicted with the exported `UmbTiptapToolbarFontFamilyExtensionApi`,
then no one could use it anyway. ¯\_(ツ)_/¯

* Tiptap extension code tidy-up

Also, makes use of `this.name` instead of hardcoded strings.
2025-08-08 08:40:35 +02:00
Lee KelleherandGitHub ba8e1ae5dd Tiptap RTE: Fixes embedded media spacing (#19878)
* Tiptap RTE: Makes embedded-media truly inline

by using a `<span>` instead of a `<div>`.

* Cosmetically aligns the selection styles

* Adds `UmbEmbeddedMediaOptions` to strongly-type the `inline` option
2025-08-08 06:52:41 +02:00
Nhu DinhandGitHub e7da631ec2 V16 Added acceptance tests for issue #17753 (#19876)
* Added tests for updating a variant block list with invalid text

* Added tests for updating a variant block grid with invalid text

* Bumped version of test helper

* Make the tests for updating content with invalid text in a block run in the pipeline

* Cleaned up

* Updated test text

* Reverted npm command
2025-08-08 02:47:28 +00:00
d23c2acd63 Fixes the Checkbox, Dropdown and Select list when the models change the UI updates. (#19487)
* Fix CheckboxList UI not updating when values are set programmatically

* WIP

* Added unit tests for the new functionality in the checkbox list element.

As requested by Copilot, here are some unit tests to ensure this addition passes all of the possible edge cases mentioned.

* Small change based on CoPilot feedback

Removed a check that was redundant and removed a unit test that was also not needed for the current PR and fixed one of the other tests.

* Fixing code quality issues highlighted in the unit tests

* Fix CheckboxList UI not updating when values are set programmatically

* WIP

* Standardizes property editor UI state management

Introduces a utility for managing the state of property editor UI elements
when their values are set programmatically.

This ensures that UI components like dropdowns, checkbox lists, and selects
correctly reflect the selected values, especially when these values are
updated via code rather than direct user interaction.

The changes include:
- A mixin to simplify state updates
- A helper function to ensure values are handled as arrays
- Consistent state updating logic across components.

* Update src/Umbraco.Web.UI.Client/src/packages/property-editors/select/property-editor-ui-select.element.ts

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

* Removed the hard coded label

* Fixed the short-circuit issue raised by co-pilot

* Fixing more co-pilot suggestions

Also cleaned up the test files based on the JSDocs suggestions.

* Update src/Umbraco.Web.UI.Client/src/packages/property-editors/dropdown/property-editor-ui-dropdown.element.ts

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

* Refactors checkbox and dropdown tests

Refactors checkbox-list and dropdown property editor UI tests to share common test utilities, reducing code duplication and improving maintainability.
Uses Sets for faster selection lookup in `updateItemsState` function.

* Fixing CodeScene suggestion based on "String Heavy Function Arguments"

* Fix for an issue that was stopping the Bellissima build.

* Improves property editor UI state updates

Ensures UI updates in checkbox list, dropdown and select property editors only occur when necessary.
Avoids unnecessary re-renders by comparing the updated state with the current state, and only triggering an update if there are actual changes.
This improves performance and prevents potential issues caused by excessive re-rendering.

* Changes based on feedback from @nielslyngsoe

* removing unnecessary call to requestUpdate

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Niels Lyngsø <nsl@umbraco.dk>
Co-authored-by: Niels Lyngsø <niels.lyngso@gmail.com>
2025-08-07 08:40:59 -04:00
Andy ButlandandGitHub a0aff9d10c Remove property value permissions when related content and/or property types are removed (#19778)
* Removed two unnecessary delete clauses when removing content types (they are looking for user group Ids, but we are deleting a content type).

* Renamed table name constant with obsoletion to better reflect name and contents of table.

* Added granular permission for property value records to delete clauses when deleting a document type.

* Delete property value permissions for removed property types.

* Added integration tests to verify behaviour.
2025-08-07 14:22:19 +02:00
8a94383262 Tiptap RTE: Refactor Table menus to use menu extension-type (#19789)
* Added `action` kind for `menuItem` extension-type

* Adds `<umb-tiptap-menu>` component

* Adds support for `menu` extensions to the `<umb-cascading-menu-popover>` component

* Adds support for `menu` extensions to the `tiptapToolbarExtension` extension-type

* Adds support for `menu` extensions to the `<umb-tiptap-toolbar-menu>` component

* Adds manifests for table column/row menus

Deprecates the `umb-tiptap-table-column-menu` and `umb-tiptap-table-row-menu` components.

* Adds table column menu actions

* Adds table row menu actions

* Adds table cell menu actions

* Adds table (general) menu actions

* Replaces table toolbar menu with the new `menu` extensions

* Adds `UMB_TIPTAP_RTE_CONTEXT`

so that the menu actions can access the Editor instance.

* Update src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/table/actions/table-properties.action.ts

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

* `UmbTiptapMenuElement` doesn't use the `editor` property

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-08-07 14:01:10 +02:00
63ed1eec41 Adds support for the "folders only" flag on retrieving siblings of a node. (#19861)
* Adds support for the "folders only" flag on retrieving siblings of a node.

* Apply suggestions from code review

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

* Updated test code.

* Removed double secondary ordering by node Id and ensured we include this clause for all sort orders.

* Ensure that ordering by node Id is always added only once and last, and only if it's not already been included in the order by clause.

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-08-07 12:35:04 +02:00
eb986d9de9 Fixed layout and formatting of saved search delete dialog on the log viewer (#19855)
* Fixed layout and formatting of saved search delete dialog.

* Import sorting

---------

Co-authored-by: leekelleher <leekelleher@gmail.com>
2025-08-07 09:50:23 +00:00
Engiber LozadaandGitHub e675894dd5 Umbraco logo label update (#19875)
* Umbraco logo label update

* Adding translation for english and spanish

* Use localize method to render text for logo
2025-08-07 08:53:41 +00:00
Nhu DinhandGitHub 3f34c8f433 V16 Added acceptance tests for regression issues (#19874)
* Updated the block editor validation message

* Updated tests for schedule publishing after unselecting all languages

* Added tests for sibdlingsOfType extension

* Updated tests due to test helper changes

* Bumped version of test helper

* Added release tag for regression issue

* Make tests for siblingsOfType run in the pipeline

* Reverted npm command
2025-08-07 08:42:55 +00:00
Andy ButlandandGitHub 44aa5dcf1c Provides an abstraction for creating the JavaScriptEncoder used in SystemTextConfigurationEditorJsonSerializer (#19849)
* Provides an abstraction for creating the JavaScriptEncoder used in SystemTextConfigurationEditorJsonSerializer.

* Generalised JSON serialization encoder factory to work for all System.Tex.Json serializers.
Added the serializer's name as a parameter to allow for different encodings per serializer if required.

* Fixed tests by removing use of obsolete constructors.

* Removed name parameter and used a generic type instead.
2025-08-07 09:08:09 +02:00
HenrikandGitHub 3a4e0b0654 Use GeneratedRegex, FrozenSet, new Lock object & static (#19872) 2025-08-07 08:41:53 +02:00
Andy ButlandandGitHub e5d1c67c36 Fix logic with check for duplicate container name at level (#19803)
Fix logic with check for duplicate container name at level.
2025-08-06 16:14:02 +02:00
Lee KelleherandGitHub 046a3d9aad Tiptap RTE: Undo deleted blocks (#19851)
* RTE: Restore deleted blocks

Maintains a state of unused (deleted) blocks,
that could be restored later, e.g. with Tiptap RTE's undo action.

Fixes #19637

* Updated with @copilot suggestions

* Fixes restored block state on variant documents
2025-08-06 15:51:21 +02:00
Jacob OvergaardandGitHub ad0854be4d build(deps): bump @umbraco-ui/uui from 1.14.2 to 1.15.0-rc.0 (#19870) 2025-08-06 14:37:15 +01:00
Mads Rasmussen 81dd568e03 fix compile error 2025-08-06 13:32:30 +02:00
Andreas ZerbstandGitHub aad253fb11 V16 QA add slack webhook for e2e tests (#19866)
* Added stage to notify slackbot

* Update nightly-E2E-test-pipelines.yml for Azure Pipelines

* Fixed indentation

* Update nightly-E2E-test-pipelines.yml for Azure Pipelines

* Updated stage name

* Added issues to condition

* Fixed SucceededWithIssues condition

* Added dependencies to condition

* Text formatting cleanup

* Clarified webhook variable name

* Updated url

* Added another url

* Added URL encoding

* Added missing $

* Added comments
2025-08-06 10:43:35 +00:00
Jacob Overgaard e3da1792cd Revert "build(deps-dev): bump storybook from 9.0.14 to 9.1.1"
This reverts commit 6c40a6f1e1.
2025-08-06 12:07:29 +02:00
36b5f63b4f Ensure block values aren't null before attempting to process them on removal (#19645)
Ensure block values aren't null before attempting to process them on removal.

Co-authored-by: Niels Lyngsø <nsl@umbraco.dk>
2025-08-06 09:10:33 +00:00
Jacob Overgaard 6c40a6f1e1 build(deps-dev): bump storybook from 9.0.14 to 9.1.1 2025-08-06 10:41:43 +02:00
Lee KelleherandGitHub c0847920f4 Tiptap RTE: Upgraded to latest v2.x (#19869)
Upgraded Tiptap to latest v2.x
2025-08-06 08:35:06 +00:00
Nhu DinhandGitHub 89a8bd3721 V16 QA Implement new release tag for E2E tests (#19697)
* Removed this as these tests are covered in other files

* Added release tags

* Make all tests with @smoke and @release tags run in the pipeline

* Updated npx command

* Updated npx command

* Updated npx command

* Fixed failed tests related to document type folder

* Cleaned up

* Used grep in yaml file instead of package.json file

* Updated yml file

* Updated testCommand

* Fixed command

* Added releaseTest command

* Added another job to run regression test in the release build

* Fixed comments

* Updated name of test job

* Make all release tests run in the pipeline

* Updated warning message

* Reverted npm command
2025-08-06 15:22:55 +07:00
Jacob OvergaardandGitHub 9414201c2e V16: Tree Stores should be optional to use (#19858)
* feat: makes tree stores optional and deprecates dependent methods

* allow `Example` as class prefix

* docs: updates example to remove the treeStore

* deprecates the usage of treeStore contexts

* chore: adds deprecation warnings to all existing tree stores
2025-08-06 08:40:42 +02:00
39b395c05d Hide property has sensitive data with normal user (#19857)
Co-authored-by: Lan Nguyen Thuy <lnt@umbraco.dk>
2025-08-06 07:07:32 +02:00
20e47d5edd Code Editor (PE): Fixes language syntax highlighting (#19864)
Fixed issue with syntax highlighting in code editor (#19414)

(cherry picked from commit 3f3c9f8823)

Co-authored-by: Andy Butland <abutland73@gmail.com>
2025-08-06 06:41:00 +02:00
71057b5f0b Build: Separate eslint logic for **/*.ts files (#19852)
* build: move typescript specific eslint rules to the `**/*ts.` pattern to avoid errors for .js files

* allow `Example` as class prefix

* allow `example-` as custom element prefix

* Removed `eslint-disable-next-line` comments

from the Example classes.

* Code formatting/tidy-up of Example classes

---------

Co-authored-by: leekelleher <leekelleher@gmail.com>
2025-08-05 11:42:39 +00:00
Andy ButlandandGitHub 0c22d512e2 Adds abstraction around boot time checks for database availability (#19848)
* Adds abstraction around boot time checks for database availability.

* Addressed issues raised in code review.
2025-08-05 13:33:18 +02:00
Andreas ZerbstandGitHub 240e155d91 16 QA added relation type tests (#19490)
* Updated relation type tests

* Created tests

* Bumped version

* Fixed tests

* Fixed tests

* Fixes based on comments

* Added waits to figure out why tests fail on pipeline

* Added a reload to check if test passes on pipeline

* Added reloads

* Removed reload page

* Reverted smokeTest command
2025-08-05 11:29:23 +00:00
Niels LyngsøandGitHub 50eeb76c5a clean up double exports (#19859) 2025-08-05 09:45:34 +00:00
fcba10aecf Retrieves item counts before and after the target for sibling endpoints and returns in API response (#19844)
* Added user start node restrictions to sibling endpoints.

* Further integration tests.

* Tidy up.

* Apply suggestions from code review

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

* Revert previous update.

* Retrieves item counts before and after the target for sibling endpoints and returns in API response.

* Applied previous update correctly.

* Removed blank line.

* Fix build and test asserts following merge.

* Update OpenApi.json.

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: mole <nikolajlauridsen@protonmail.ch>
2025-08-05 11:14:59 +02:00
20254f0bbc Added user start node restrictions to sibling endpoints (#19839)
* Added user start node restrictions to sibling endpoints.

* Further integration tests.

* Tidy up.

* Apply suggestions from code review

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

* Revert previous update.

* Applied previous update correctly.

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-08-05 09:53:39 +02:00
f5ff2bbf59 Fix issue create media folder throw error (#19854)
Co-authored-by: Lan Nguyen Thuy <lnt@umbraco.dk>
2025-08-05 07:35:46 +02:00
Jacob Overgaard c31861945c update package lockfile 2025-08-04 16:04:42 +02:00
Andy ButlandandGitHub af8742651c Umbraco Engage UmbracoUrlAlias Fix - Fixes #19654 (#19827) (#19850)
* Fixes #19654

Adds the propertyAlias to the VariationContext so that products implementing the GetSegment method are aware which propertyAlias it's being called for

* Re-implement original variation context for backwards compatibility

* Fixes hidden overload method

Ensures the `GetSegment` method overload is not hidden when a null `propertyAlias` is passed.

* Resolve backward compatibility issues.

* Improved comments.

---------


# Conflicts:
#	src/Umbraco.PublishedCache.NuCache/Property.cs
2025-08-04 13:56:37 +02:00
4bf2fbf1ba Bugfix/19601 can not add ef core migrations (#19846)
* fix EFCore add migration issue

* update test

* Resolved breaking changes and code review comments.

* Removed extra line break.

---------

Co-authored-by: Andy Butland <abutland73@gmail.com>
2025-08-04 10:56:25 +00:00
7c9c7337b9 Add pagination and total to examine dashboard (#19847)
* Add pagination and total to examine dashboard

* fix name and localization

---------

Co-authored-by: Lan Nguyen Thuy <lnt@umbraco.dk>
2025-08-04 10:44:56 +00:00
a630febf67 Docs: Storybook clean up round 1 (#19673)
* delete internal stories

* more clean up

* more cleanup

* move to generic components

* clean up

* move body layout

* move story

* Move icon stories

* remove prefilled color

* Update icon.element.ts

* rename story

* Replace UUIFormControlMixin with UmbFormControlMixin

Refactors all relevant input and form control components to use the new UmbFormControlMixin from '@umbraco-cms/backoffice/validation' instead of the deprecated UUIFormControlMixin. This change improves consistency and aligns with updated validation handling in the codebase.

* Revert "Merge branch 'v16/bugfix/use-umb-form-control-mixin' into v16/docs/storybook-clean-up"

This reverts commit 7fa70b87c7, reversing
changes made to 8fe7391790.

* simplify name

* Add discard changes modal stories

* add error viewer modal

* fix stories

* rename

* fix date story

* add story for input with alias

* add story for popover layout

* add story for dropdown

* add args

* register core manifests

* register entity action bundle + list

* add stack example

* clean up

* Create data-type-input.stories.ts

* change overview story to docs

* rename to docs

* rename to docs

* Update icon.stories.ts

* Update preview.js

* remove overview story

* rename default story

* load more manifests

* import all manifests

* Update preview.js

* Update preview.js

* provide all stores + global contexts

* Update data-type-input.stories.ts

* add user input and ref stories

* add storybook auth context

* set the initial storybook language

* use isoCode param

* fix input-language component

* delete broken stories

* fix icon picker story

* fix mock member ids

* Fix query parameter name in item handler

Changed the query parameter from 'paths' to 'path' in the item handler to correctly retrieve item paths from the request. This ensures the handler processes requests as expected.

* Update user item handler to use user mock DB

Replaces the document mock database with the user mock database in the user item handler to ensure correct data source is used for user-related requests.

* Add config to checkbox list story

Introduces a sample configuration to the checkbox list Storybook story, providing predefined options for demonstration and testing purposes.

* Add config to select property editor story

Introduces a sample UmbPropertyEditorConfigCollection to the select property editor Storybook story, providing predefined options for demonstration and testing purposes.

* Add config to radio button list Storybook story

Introduces a sample UmbPropertyEditorConfigCollection to the radio button list

* Refactor slider story to use config collection

Replaces inline config array with UmbPropertyEditorConfigCollection for the slider property editor story.

* Delete property-editor-ui-label.stories.ts

* Group releated UIs

* Remove multi-url picker Storybook file

* add input stylesheet story

* add back localization stories

* Delete property-editor-config.stories.ts

---------

Co-authored-by: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com>
2025-08-04 09:27:50 +02:00
dependabot[bot]andJacob Overgaard 170becdd5b Bump linkifyjs from 4.3.1 to 4.3.2 in /src/Umbraco.Web.UI.Login
Bumps [linkifyjs](https://github.com/nfrasser/linkifyjs/tree/HEAD/packages/linkifyjs) from 4.3.1 to 4.3.2.
- [Release notes](https://github.com/nfrasser/linkifyjs/releases)
- [Changelog](https://github.com/nfrasser/linkifyjs/blob/main/CHANGELOG.md)
- [Commits](https://github.com/nfrasser/linkifyjs/commits/v4.3.2/packages/linkifyjs)

---
updated-dependencies:
- dependency-name: linkifyjs
  dependency-version: 4.3.2
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-08-04 09:25:58 +02:00
hifi-philandGitHub 39a5362de8 Add testing to workspace counter examples (#19838)
* update workspace example

* Update readme for workspace counter example

* update workspace counter examples readme

* Update examples workspace counter to include some testing

* Update glob pattern for text examples for windows
2025-08-02 12:36:23 +02:00
de64c53777 Omit datetime from JSON conversion that converts from strings (#19807)
* Remove date object conversion as valueEditors don't seem to need it

* Update fault summary reference

* Added justification comment.

---------

Co-authored-by: Andy Butland <abutland73@gmail.com>
2025-08-01 13:27:05 +02:00
d9e7e9e8a8 Make logging more friendly for docker (#19818)
* Cleanup obsoleted methods

* Add a way to disable UmbracoFile default sink

* Abstract LogViewService so only UmbracoFile sink related things are in the default interface implementation.

* Abstract LogViewRepository so only UmbracoFile sink related things are in the default interface implementation.

* Move GetGlobalLogLevelEventMinLevel to base

* Removed unused internal class and obsoleted its base

* Added missing XML header comments and resolved warnings in service and repository classes.

* Made private method static.

* Addressed issues raised in code review.

* Expose repository from the service base class.

* Restored further obsoleted code we can't remove yet.

* Removed log viewer tests on removed class. We have integration tests for the new service.

* Obsoleted ILogViewer interface.

---------

Co-authored-by: Andy Butland <abutland73@gmail.com>
2025-08-01 11:29:58 +02:00
Lee KelleherandGitHub a1d1c5492a Tiptap RTE: Custom CSS Variables for min/max height/width (#19841)
* Adds CSS variables to `umb-input-tiptap`

to set the min/max height/width of the RTE.

* Moves "dimensions" config to the base RTE element

so can be reused with other RTE-based property-editors.

* Sets the CSS variables in the Tiptap property-editor element

* Code tidyup for RTE base element imports

* Corrects localization text of RTE dimensions description

As it's a fixed height/width as opposed to a maximum height/width.

* The CSS variable fallback value 'unset' should not be quoted.

CSS keywords like 'unset' should be unquoted, while string values should be quoted.
2025-08-01 07:16:02 +02:00
Lee KelleherandGitHub 7199ed0310 Media Table Collection View: Adds support for Label Templates (#19842)
Media Table Collection View: add label template support

Fixes #19835.
2025-08-01 06:49:33 +02:00
615fcddcf7 V16 QA Updated acceptance tests with skip() and fixme() tag (#19397)
* Updated nightly E2E pipeline

* Fixed failing E2E tests

* Skipped content tests wirh list view content due to an issue

* Updated tests due to UI changes

* Updated fixme and skip tests - part 1

* Removed this file because the tests are already covered elsewhere

* Updated fixme() tests

* Updated skip() tests

* Bumped version

* Bumped version

* Bumped version

* Removed notification verification

* Removed the step to verify the notification for save action

* Fixed the failing tests

* Updated name of permission

* Bumped version

* Fixed failing tests

* Bumped testHelpers

* Removed tests related to tiptap toolbar as they are covered in another class

* Cleaned up

* Added more waits

* Cleaned up

* Added step to ensure redirect URL is created when renaming content.

* Restructured the tests

* Removed unnecessary steps

* Fixed isItemVisibleInRecycleBin

* Fixed isItemVisibleInRecycleBin

* Bumped version

* Added more wait for the deletion to complete

* Added waits for the deletion to complete

* Added more waits

* Removed unnecessary waits

* Added more waits to improve test stability

* Added skip for the flaky test

* Added test for removing a stylesheet in a block grid editor

* Updated test due to api helper changes

* Bumped version

* Fixed failing smoke test

---------

Co-authored-by: Andreas Zerbst <andr317c@live.dk>
2025-08-01 03:30:03 +00:00
8d6096636d Add drag and drop to blockgrid area (#19383)
* Add drag and drop to blockgrid area

* Adds `UmbChangeEvent` trigger

* Removes `updated` method

Puts `sorter.setModel` in the `value` setter,
so that the sorter is set on initial value.

* Imports sort order

Removed `UmbTextStyles`, as not used here

* Changed the cursor type to "move"

---------

Co-authored-by: Lan Nguyen Thuy <lnt@umbraco.dk>
Co-authored-by: Niels Lyngsø <nsl@umbraco.dk>
Co-authored-by: leekelleher <leekelleher@gmail.com>
2025-07-31 16:17:13 +00:00
4483f47668 V16: Fix issue not display hidden tab name (#19370)
Fix issue not display hidden tab name

Co-authored-by: Lan Nguyen Thuy <lnt@umbraco.dk>
Co-authored-by: leekelleher <leekelleher@gmail.com>
2025-07-31 14:29:41 +00:00
Andrej DavidovicandGitHub ce7ce10d64 Update RichTextPropertyEditor.cs (#19826)
Fix for https://github.com/umbraco/Umbraco-CMS/issues/19824 issue
2025-07-31 14:11:06 +00:00
965408df57 Tiptap RTE: Block selection (#19811)
* Removed `cursor: not-allowed` style

* Sets the `umb-rte-block` `user-select` to `all`

* Adds an "invisible" selection background to `umb-ref-rte-block`

* Sets the `umb-ufm-render` text-content to be visible

* Adds `aria-hidden` attribute

* Bumped version of test helper

* Fixed the failing tests due to UI changes

* Adds `pointer-events: none` to selection-background

---------

Co-authored-by: Nhu Dinh <hnd@umbraco.dk>
2025-07-31 09:39:30 +00:00
Andy Butland 4a37f06677 Merge branch 'main' of https://github.com/umbraco/Umbraco-CMS 2025-07-31 06:59:22 +02:00
Andy Butland 300f507efc Update LTS version on template. 2025-07-31 06:59:11 +02:00
Nhu DinhandGitHub 345afd243e V16 QA Added acceptance tests for the block with RTE in a Content (#19795)
* Updated tests for content with RTE in a block grid

* Updated tests forcontent with RTE in a block list

* Make all RTE tests run in the pipeline

* Cleaned up

* Reverted npm command
2025-07-31 04:58:51 +00:00
hifi-philandGitHub 133796f2dd V16/docs work extensions example (#19809)
* update workspace example

* Update readme for workspace counter example

* update workspace counter examples readme
2025-07-30 07:44:23 +02:00
34989307db Change hardcoded text to be translatedeable (#19745)
* Change hardcoded text to be translatedeable

* Added the `count` value to the localization

---------

Co-authored-by: Lucas Bach Bisgaard <lucas.bisgaard@kraftvaerk.com>
Co-authored-by: leekelleher <leekelleher@gmail.com>
2025-07-29 17:01:22 +00:00
Lee KelleherandGitHub fb9a9b38a8 Tiptap RTE: Include Tiptap's default styles (#19805)
* Disables Tiptap's `injectCSS` option

This option would inject the default CSS styles into
the `window.document`, which are never applied to
the component's shadow DOM.

* Add Tiptap's default styles to "rte-content.css"

The `caret-color` rule (line 93) resolves issue #19791.
2025-07-29 07:58:41 +02:00
Andy Butland 66b2e3b421 Merge branch 'main' of https://github.com/umbraco/Umbraco-CMS 2025-07-29 07:23:25 +02:00
Andy Butland bc3e3d789d Merge branch 'release/16.1.1'
# Conflicts:
#	src/Umbraco.Web.UI.Client/package.json
#	version.json
2025-07-29 07:23:14 +02:00
75c7d00b53 Fix issue unsaved changes always displayed when trying to move away from blueprint (#19804)
Co-authored-by: Lan Nguyen Thuy <lnt@umbraco.dk>
2025-07-29 07:21:17 +02:00
7e82c258ee Merge commit from fork
Co-authored-by: kjac <kja@umbraco.dk>
2025-07-29 05:10:52 +02:00
a2cc6a0a87 Fix issue with use of EF Core scopes within notification handlers (take 2 - handling scopes with a base parent) (#19797)
* Add integration tests that shows the problem

* Fix the problem and add explenation

* Improved comments slightly to help when we come back here!
Moved tests alongside existing ones related to scopes.
Removed long running attribute from tests (they are quite fast).

* Fixed casing in comment.

---------

Co-authored-by: Andy Butland <abutland73@gmail.com>
2025-07-25 13:07:20 +02:00
Andy Butland 0b42085353 Bumped version to 16.1.1. 2025-07-24 09:25:52 +02:00
Andy Butland 5c57d03d6d Merge branch 'release/16.1'
# Conflicts:
#	version.json
2025-07-24 09:22:37 +02:00
Lee KelleherandGitHub 12141d2d61 Tiptap RTE: Clear Formatting, resets nodes to "paragraph" (#19781)
Fixes #19752
2025-07-24 07:07:29 +02:00
Andy Butland a5612107a6 Bumped version to 16.1.0. 2025-07-24 06:48:27 +02:00
Laura NetoandGitHub c20b30d625 Fix null reference exception when removing all blocks from shared RTE in culture variant content (#19771)
Fix null reference exception when removing all blocks from culture variant content
2025-07-23 20:10:02 +02:00
be8e0f319d Fix navigationUrlService and underlying models not being thread safe (#19689)
* Fix navigationUrlService and underlying models not being thread safe

* Added migration to plan.

---------

Co-authored-by: Andy Butland <abutland73@gmail.com>
2025-07-23 11:52:08 +00:00
0507d1a0c8 Fix missing tag suggestions when no culture available. (#19694)
Allow undefined culture when obtaining tag matches

Co-authored-by: Ben White <b.white@rocburn.com>
2025-07-23 12:25:00 +02:00
73c995c8ec #19775 fixed get user data by applying OrderBy after counting (#19776)
* #19775 fixed get user data by applying OrderBy after counting

* Apply suggestions from code review

---------

Co-authored-by: Andy Butland <abutland73@gmail.com>
2025-07-23 11:33:49 +02:00
Andy ButlandandGitHub 5e68bf7c21 Log the migration plan name when can't migrate from state (#19772)
Log the migration plan name when can't migrate from state.
2025-07-22 17:00:44 +00:00
6a5b1a76cc V16 QA Added acceptance tests for the 'create document' user permission (#19770)
* Remove skip

* Added tests for creating and updating content

* Removed skip because the issue is fixed

* Updated assertion steps for the update document user permission

* Bumped version

* Added release tag

---------

Co-authored-by: Nikolaj Geisle <70372949+Zeegaan@users.noreply.github.com>
2025-07-22 15:57:32 +02:00
b722c0d72d Abstract submit and poll operations (#19688)
* Started implementing new LongRunningOperationService and adjusting tasks to use this service

This service will manage operations that require status to be synced between servers (load balanced setup).

* Missing migration to add new lock. Other simplifications.

* Add job to cleanup the LongRunningOperations entries

* Add new DatabaseCacheRebuilder.RebuildAsync method

This is both async and returns an attempt, which will fail if a rebuild operation is already running.

* Missing LongRunningOperation database table creation on clean install

* Store expire date in the long running operation. Better handling of non-background operations.

Storing an expiration date allows setting different expiration times depending on the type of operation, and whether it is running in the background or not.

* Added integration tests for LongRunningOperationRepository

* Added unit tests for LongRunningOperationService

* Add type as a parameter to more repository calls. Distinguish between expiration and deletion in `LongRunningOperationRepository.CleanOperations`.

* Fix failing unit test

* Fixed `PerformPublishBranchAsync` result not being deserialized correctly

* Remove unnecessary DatabaseCacheRebuildResult value

* Add status to `LongRunningOperationService.GetResult` attempt to inform on why a result could not be retrieved

* General improvements

* Missing rename

* Improve the handling of long running operations that are not in background and stale operations

* Fix failing unit tests

* Fixed small mismatch between interface and implementation

* Use a fire and forget task instead of the background queue

* Apply suggestions from code review

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

* Make sure exceptions are caught when running in the background

* Alignment with other repositories (async + pagination)

* Additional fixes

* Add Async suffix to service methods

* Missing adjustment

* Moved hardcoded settings to IOptions

* Fix issue in SQL Server where 0 is not accepted as requested number of rows

* Fix issue in SQL Server where query provided to count cannot contain orderby

* Additional SQL Server fixes

---------

Co-authored-by: Andy Butland <abutland73@gmail.com>
2025-07-22 15:26:04 +02:00
ca1476f7c7 Fires the updated event when content types are updated (#19768)
* Fires the updated event when content types are updated.

* Housekeeping: separating/sorting import types

---------

Co-authored-by: leekelleher <leekelleher@gmail.com>
2025-07-22 12:22:11 +00:00
d7a32a31a1 Reloads the template tree when creating a document type with a template (#19769)
* Reloads the template tree when creating a document type with a template.

* Housekeeping: separating/sorting import types

---------

Co-authored-by: leekelleher <leekelleher@gmail.com>
2025-07-22 10:46:04 +00:00
Lee KelleherandGitHub df9b387175 Repository Details Manager: Prevent making requests for empty arrays (#19731)
* fix: Prevent Repository Details Manager making requests for empty arrays

Fixes #19604

* Reworked to pass the `uniques` through to the `#requestNewDetails()` method

The unique values are included as a closure,
persisting after the `#init` promise is resolved.
Rather than call `getUniques()` to get an async'd value.

* Updated with Copilot suggestions

https://github.com/umbraco/Umbraco-CMS/pull/19731#discussion_r2221512463
2025-07-22 08:16:38 +00:00
Andy Butlandandkjac 8cc6508b22 Retrieve only user external logins when invalidate following removal of backoffice external user login (#19766)
* Retrieve only user external logins when invalidate following removal of backoffice external user login.

* Improved variable name.
2025-07-22 10:06:41 +02:00
a0406b1406 Add defensive coding to the member application initializer (16) (#19764)
* Add defensive coding to the member application initializer (#19760)

* Moved _isInitialized to after the initialization

---------

Co-authored-by: kjac <kja@umbraco.dk>
2025-07-21 11:19:42 +00:00
93d61d0316 Return 404 on delivery API requests for segments that are invalid or not created (#19718)
* Return 404 on delivery API requests for segments that are invalid or not created.

* Handled case with no segmented properties.

* Let the property decide if it has a value or not

---------

Co-authored-by: kjac <kja@umbraco.dk>
2025-07-21 13:05:40 +02:00
Kenn JacobsenandGitHub 55cc415c30 Make the OpenAPI security scheme name for the Management API valid (#19762) 2025-07-21 12:51:44 +02:00
Lee KelleherandGitHub 1ec9641c9d Updated OpenApi and TypeScript API (Siblings endpoints) (#19759)
Latest OpenApi TypeScript API code generation
2025-07-21 08:40:29 +00:00
5c05fd2877 Template with Block List field with Inline Editing Mode causes Collection List View to shrink (#19754)
Fix issue template is shrunk when enable inline editing mode in collection list view in block list field

Co-authored-by: Lan Nguyen Thuy <lnt@umbraco.dk>
2025-07-21 10:17:02 +02:00
6fe39b071f Add a backing field for EditorUIAlias and track changes when its set. (#19733)
* Add a backing field for EditorUIAlias and track changes when its set.

* Add previously failing unit test to verify fix.

* Aligned backing field casing with property name.

---------

Co-authored-by: Andy Butland <abutland73@gmail.com>
2025-07-21 07:52:58 +00:00
Andy Butland 7458143bb8 Restored indentation following merge of Danish translations in #19740. 2025-07-21 09:27:54 +02:00
2a83e405a7 add missing translation for DA (#19740)
Co-authored-by: Lucas Bach Bisgaard <lucas.bisgaard@kraftvaerk.com>
2025-07-21 09:26:30 +02:00
HenrikandGitHub 7f88013689 Make internal & private classes sealed where possible, to avoid code for virtual dispatch (#19719) 2025-07-21 08:32:54 +02:00
Laura NetoandGitHub 0bcc26a811 Allow saving blueprints with validation errors (#19748)
Ignore validation result when saving blueprint
2025-07-21 07:55:12 +02:00
1255 changed files with 14360 additions and 7337 deletions
+174 -4
View File
@@ -513,9 +513,9 @@ stages:
UMBRACO__CMS__WEBROUTING__UMBRACOAPPLICATIONURL: https://localhost:44331/
ASPNETCORE_URLS: https://localhost:44331
jobs:
# E2E Tests
# E2E Smoke Tests
- job:
displayName: E2E Tests (SQLite)
displayName: E2E Smoke Tests (SQLite)
# currently disabled due to DB locks randomly occuring.
condition: eq(${{parameters.sqliteAcceptanceTests}}, True)
variables:
@@ -678,7 +678,7 @@ stages:
testRunTitle: "$(Agent.JobName)"
- job:
displayName: E2E Tests (SQL Server)
displayName: E2E Smoke Tests (SQL Server)
variables:
# Connection string
CONNECTIONSTRINGS__UMBRACODBDSN: Data Source=(localdb)\MSSQLLocalDB;AttachDbFilename=|DataDirectory|\Umbraco.mdf;Integrated Security=True
@@ -862,6 +862,176 @@ stages:
searchFolder: "tests/Umbraco.Tests.AcceptanceTest/results"
testRunTitle: "$(Agent.JobName)"
- job:
displayName: E2E Release Tests (SQL Server)
variables:
# Connection string
CONNECTIONSTRINGS__UMBRACODBDSN: Data Source=(localdb)\MSSQLLocalDB;AttachDbFilename=|DataDirectory|\Umbraco.mdf;Integrated Security=True
CONNECTIONSTRINGS__UMBRACODBDSN_PROVIDERNAME: Microsoft.Data.SqlClient
condition: eq(dependencies.Build.outputs['A.build.NBGV_PublicRelease'], 'True')
strategy:
matrix:
WindowsPart1Of3:
vmImage: "windows-latest"
testCommand: "npm run releaseTest -- --shard=1/3"
WindowsPart2Of3:
vmImage: "windows-latest"
testCommand: "npm run releaseTest -- --shard=2/3"
WindowsPart3Of3:
vmImage: "windows-latest"
testCommand: "npm run releaseTest -- --shard=3/3"
pool:
vmImage: $(vmImage)
steps:
# Setup test environment
- task: DownloadPipelineArtifact@2
displayName: Download NuGet artifacts
inputs:
artifact: nupkg
path: $(Agent.BuildDirectory)/app/nupkg
- task: NodeTool@0
displayName: Use Node.js $(nodeVersion)
inputs:
versionSpec: $(nodeVersion)
- task: UseDotNet@2
displayName: Use .NET SDK from global.json
inputs:
useGlobalJson: true
- pwsh: |
"UMBRACO_USER_LOGIN=$(UMBRACO__CMS__UNATTENDED__UNATTENDEDUSEREMAIL)
UMBRACO_USER_PASSWORD=$(UMBRACO__CMS__UNATTENDED__UNATTENDEDUSERPASSWORD)
URL=$(ASPNETCORE_URLS)
STORAGE_STAGE_PATH=$(Build.SourcesDirectory)/tests/Umbraco.Tests.AcceptanceTest/playwright/.auth/user.json
CONSOLE_ERRORS_PATH=$(Build.SourcesDirectory)/tests/Umbraco.Tests.AcceptanceTest/console-errors.json" | Out-File .env
displayName: Generate .env
workingDirectory: $(Build.SourcesDirectory)/tests/Umbraco.Tests.AcceptanceTest
# Cache and restore NPM packages
- task: Cache@2
displayName: Cache NPM packages
inputs:
key: 'npm_e2e | "$(Agent.OS)" | $(Build.SourcesDirectory)/tests/Umbraco.Tests.AcceptanceTest/package-lock.json'
restoreKeys: |
npm_e2e | "$(Agent.OS)"
npm_e2e
path: $(npm_config_cache)
- script: npm ci --no-fund --no-audit --prefer-offline
workingDirectory: $(Build.SourcesDirectory)/tests/Umbraco.Tests.AcceptanceTest
displayName: Restore NPM packages
# Build application
- pwsh: |
$cmsVersion = "$(Build.BuildNumber)" -replace "\+",".g"
dotnet new nugetconfig
dotnet nuget add source ./nupkg --name Local
dotnet new install Umbraco.Templates::$cmsVersion
dotnet new umbraco --name UmbracoProject --version $cmsVersion --exclude-gitignore --no-restore --no-update-check
dotnet restore UmbracoProject
cp $(Build.SourcesDirectory)/tests/Umbraco.Tests.AcceptanceTest.UmbracoProject/*.cs UmbracoProject
dotnet build UmbracoProject --configuration $(buildConfiguration) --no-restore
dotnet dev-certs https
displayName: Build application
workingDirectory: $(Agent.BuildDirectory)/app
# Start SQL Server
- powershell: docker run --name mssql -d -p 1433:1433 -e "ACCEPT_EULA=Y" -e "MSSQL_SA_PASSWORD=$(SA_PASSWORD)" mcr.microsoft.com/mssql/server:2022-latest
displayName: Start SQL Server Docker image (Linux)
condition: and(succeeded(), eq(variables['Agent.OS'], 'Linux'))
- pwsh: SqlLocalDB start MSSQLLocalDB
displayName: Start SQL Server LocalDB (Windows)
condition: and(succeeded(), eq(variables['Agent.OS'], 'Windows_NT'))
# Run application
- bash: |
nohup dotnet run --project UmbracoProject --configuration $(buildConfiguration) --no-build --no-launch-profile > $(Build.ArtifactStagingDirectory)/playwright.log 2>&1 &
echo "##vso[task.setvariable variable=AcceptanceTestProcessId]$!"
displayName: Run application (Linux)
condition: and(succeeded(), eq(variables['Agent.OS'], 'Linux'))
workingDirectory: $(Agent.BuildDirectory)/app
- pwsh: |
$process = Start-Process dotnet "run --project UmbracoProject --configuration $(buildConfiguration) --no-build --no-launch-profile 2>&1" -PassThru -NoNewWindow -RedirectStandardOutput $(Build.ArtifactStagingDirectory)/playwright.log
Write-Host "##vso[task.setvariable variable=AcceptanceTestProcessId]$($process.Id)"
displayName: Run application (Windows)
condition: and(succeeded(), eq(variables['Agent.OS'], 'Windows_NT'))
workingDirectory: $(Agent.BuildDirectory)/app
# Wait for application to start responding to requests
- pwsh: npx wait-on -v --interval 1000 --timeout 120000 $(ASPNETCORE_URLS)
displayName: Wait for application
workingDirectory: tests/Umbraco.Tests.AcceptanceTest
# Install Playwright and dependencies
- pwsh: npx playwright install chromium
displayName: Install Playwright only with Chromium browser
workingDirectory: tests/Umbraco.Tests.AcceptanceTest
# Test
- pwsh: $(testCommand)
displayName: Run Playwright tests
workingDirectory: tests/Umbraco.Tests.AcceptanceTest
env:
CI: true
CommitId: $(Build.SourceVersion)
AgentOs: $(Agent.OS)
# Stop application
- bash: kill -15 $(AcceptanceTestProcessId)
displayName: Stop application (Linux)
condition: and(ne(variables.AcceptanceTestProcessId, ''), eq(variables['Agent.OS'], 'Linux'))
- pwsh: Stop-Process -Id $(AcceptanceTestProcessId)
displayName: Stop application (Windows)
condition: and(ne(variables.AcceptanceTestProcessId, ''), eq(variables['Agent.OS'], 'Windows_NT'))
# Stop SQL Server
- pwsh: docker stop mssql
displayName: Stop SQL Server Docker image (Linux)
condition: eq(variables['Agent.OS'], 'Linux')
- pwsh: SqlLocalDB stop MSSQLLocalDB
displayName: Stop SQL Server LocalDB (Windows)
condition: eq(variables['Agent.OS'], 'Windows_NT')
# Copy artifacts
- pwsh: |
if (Test-Path tests/Umbraco.Tests.AcceptanceTest/results/*) {
Copy-Item tests/Umbraco.Tests.AcceptanceTest/results/* $(Build.ArtifactStagingDirectory) -Recurse
}
displayName: Copy Playwright results
condition: succeededOrFailed()
# Copy console error log
- pwsh: |
if (Test-Path tests/Umbraco.Tests.AcceptanceTest/console-errors.json) {
Copy-Item tests/Umbraco.Tests.AcceptanceTest/console-errors.json $(Build.ArtifactStagingDirectory)
}
displayName: Copy console error log
condition: succeededOrFailed()
# Publish test artifacts
- task: PublishPipelineArtifact@1
displayName: Publish test artifacts
condition: succeededOrFailed()
inputs:
targetPath: $(Build.ArtifactStagingDirectory)
artifact: "Acceptance Test Results - $(Agent.JobName) - Attempt #$(System.JobAttempt)"
# Publish test results
- task: PublishTestResults@2
displayName: "Publish test results"
condition: succeededOrFailed()
inputs:
testResultsFormat: 'JUnit'
testResultsFiles: '*.xml'
searchFolder: "tests/Umbraco.Tests.AcceptanceTest/results"
testRunTitle: "$(Agent.JobName)"
###############################################
## Release
###############################################
@@ -1067,4 +1237,4 @@ stages:
storage: umbracoapidocs
ContainerName: "$web"
BlobPrefix: v$(umbracoMajorVersion)/ui-api
CleanTargetBeforeCopy: true
CleanTargetBeforeCopy: true
+49 -2
View File
@@ -9,11 +9,10 @@ schedules:
branches:
include:
- v15/dev
- release/16.0
- main
parameters:
# Skipped due to DB locks
# Skipped due to DB locks
- name: sqliteAcceptanceTests
displayName: Run SQLite Acceptance Tests
type: boolean
@@ -484,3 +483,51 @@ stages:
testResultsFiles: '*.xml'
searchFolder: "tests/Umbraco.Tests.AcceptanceTest/results"
testRunTitle: "$(Agent.JobName)"
- stage: NotifySlackBot
displayName: Notify Slack on Failure
dependsOn: E2E
# This stage will only run if the E2E tests fail or succeed with issues
condition: or(
eq(dependencies.E2E.result, 'failed'),
eq(dependencies.E2E.result, 'succeededWithIssues'))
jobs:
- job: PostToSlack
displayName: Send Slack Notification
pool:
vmImage: 'ubuntu-latest'
steps:
# We send a payload to the Slack webhook URL, which will post a message to a specific channel
- bash: |
PROJECT_NAME_ENCODED=$(echo -n "$SYSTEM_TEAMPROJECT" | jq -s -R -r @uri)
PIPELINE_URL="${SYSTEM_TEAMFOUNDATIONCOLLECTIONURI}${PROJECT_NAME_ENCODED}/_build/results?buildId=${BUILD_BUILDID}&view=ms.vss-test-web.build-test-results-tab"
PAYLOAD="{
\"attachments\": [
{
\"color\": \"#ff0000\",
\"pretext\": \"Nightly E2E pipeline *${BUILD_DEFINITIONNAME}* (#${BUILD_BUILDNUMBER}) failed!\",
\"title\": \"View Failed E2E Test Results\",
\"title_link\": \"$PIPELINE_URL\",
\"fields\": [
{
\"title\": \"Pipeline\",
\"value\": \"${BUILD_DEFINITIONNAME}\",
\"short\": true
},
{
\"title\": \"Build ID\",
\"value\": \"${BUILD_BUILDID}\",
\"short\": true
}
]
}
]
}"
echo "Sending Slack message to: $PIPELINE_URL"
curl -X POST -H 'Content-type: application/json' \
--data "$PAYLOAD" \
"$SLACK_WEBHOOK_URL"
env:
SLACK_WEBHOOK_URL: $(E2ESLACKWEBHOOKURL)
@@ -4,7 +4,7 @@ using Umbraco.Cms.Core.Configuration.Models;
namespace Umbraco.Cms.Api.Common.Configuration;
internal class ConfigureOpenIddict : IConfigureOptions<OpenIddictServerAspNetCoreOptions>
internal sealed class ConfigureOpenIddict : IConfigureOptions<OpenIddictServerAspNetCoreOptions>
{
private readonly IOptions<GlobalSettings> _globalSettings;
@@ -4,7 +4,7 @@ using Microsoft.Extensions.Logging;
namespace Umbraco.Cms.Api.Common.Json;
internal class NamedSystemTextJsonInputFormatter : SystemTextJsonInputFormatter
internal sealed class NamedSystemTextJsonInputFormatter : SystemTextJsonInputFormatter
{
private readonly string _jsonOptionsName;
@@ -3,8 +3,7 @@ using Microsoft.AspNetCore.Mvc.Formatters;
namespace Umbraco.Cms.Api.Common.Json;
internal class NamedSystemTextJsonOutputFormatter : SystemTextJsonOutputFormatter
internal sealed class NamedSystemTextJsonOutputFormatter : SystemTextJsonOutputFormatter
{
private readonly string _jsonOptionsName;
@@ -0,0 +1,17 @@
using System.ComponentModel.DataAnnotations;
namespace Umbraco.Cms.Api.Common.ViewModels.Pagination;
public class SubsetViewModel<T>
{
[Required]
public long TotalBefore { get; set; }
[Required]
public long TotalAfter { get; set; }
[Required]
public IEnumerable<T> Items { get; set; } = Enumerable.Empty<T>();
public static SubsetViewModel<T> Empty() => new();
}
@@ -23,7 +23,12 @@ internal sealed class DeliveryApiOutputCachePolicy : IOutputCachePolicy
.RequestServices
.GetRequiredService<IRequestPreviewService>();
context.EnableOutputCaching = requestPreviewService.IsPreview() is false;
IApiAccessService apiAccessService = context
.HttpContext
.RequestServices
.GetRequiredService<IApiAccessService>();
context.EnableOutputCaching = requestPreviewService.IsPreview() is false && apiAccessService.HasPublicAccess();
context.ResponseExpirationTimeSpan = _duration;
context.CacheVaryByRules.HeaderNames = _varyByHeaderNames;
@@ -26,7 +26,7 @@ public class ConfigureUmbracoMemberAuthenticationDeliveryApiSwaggerGenOptions :
options.OperationFilter<DeliveryApiSecurityFilter>();
}
private class DeliveryApiSecurityFilter : SwaggerFilterBase<ContentApiControllerBase>, IOperationFilter, IDocumentFilter
private sealed class DeliveryApiSecurityFilter : SwaggerFilterBase<ContentApiControllerBase>, IOperationFilter, IDocumentFilter
{
public void Apply(OpenApiOperation operation, OperationFilterContext context)
{
@@ -13,7 +13,7 @@ internal sealed class ContextualizeFromAcceptHeadersAttribute : TypeFilterAttrib
{
}
private class LocalizeFromAcceptLanguageHeaderAttributeFilter : IActionFilter
private sealed class LocalizeFromAcceptLanguageHeaderAttributeFilter : IActionFilter
{
private readonly IRequestCultureService _requestCultureService;
private readonly IRequestSegmmentService _requestSegmentService;
@@ -11,7 +11,7 @@ internal sealed class DeliveryApiAccessAttribute : TypeFilterAttribute
{
}
private class DeliveryApiAccessFilter : IActionFilter
private sealed class DeliveryApiAccessFilter : IActionFilter
{
private readonly IApiAccessService _apiAccessService;
private readonly IRequestPreviewService _requestPreviewService;
@@ -11,7 +11,7 @@ internal sealed class DeliveryApiMediaAccessAttribute : TypeFilterAttribute
{
}
private class DeliveryApiMediaAccessFilter : IActionFilter
private sealed class DeliveryApiMediaAccessFilter : IActionFilter
{
private readonly IApiAccessService _apiAccessService;
@@ -12,7 +12,7 @@ internal sealed class ValidateStartItemAttribute : TypeFilterAttribute
{
}
private class ValidateStartItemFilter : IActionFilter
private sealed class ValidateStartItemFilter : IActionFilter
{
private readonly IRequestStartItemProviderAccessor _requestStartItemProviderAccessor;
@@ -7,6 +7,7 @@ using Umbraco.Cms.Core.Events;
using Umbraco.Cms.Core.Notifications;
using Umbraco.Cms.Core.Security;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Sync;
using Umbraco.Cms.Infrastructure.Security;
namespace Umbraco.Cms.Api.Delivery.Handlers;
@@ -18,18 +19,24 @@ internal sealed class InitializeMemberApplicationNotificationHandler : INotifica
private readonly DeliveryApiSettings _deliveryApiSettings;
private readonly IServiceScopeFactory _serviceScopeFactory;
private readonly IMemberClientCredentialsManager _memberClientCredentialsManager;
private readonly IServerRoleAccessor _serverRoleAccessor;
private static readonly SemaphoreSlim _locker = new(1);
private static bool _isInitialized = false;
public InitializeMemberApplicationNotificationHandler(
IRuntimeState runtimeState,
IOptions<DeliveryApiSettings> deliveryApiSettings,
ILogger<InitializeMemberApplicationNotificationHandler> logger,
IServiceScopeFactory serviceScopeFactory,
IMemberClientCredentialsManager memberClientCredentialsManager)
IMemberClientCredentialsManager memberClientCredentialsManager,
IServerRoleAccessor serverRoleAccessor)
{
_runtimeState = runtimeState;
_logger = logger;
_serviceScopeFactory = serviceScopeFactory;
_memberClientCredentialsManager = memberClientCredentialsManager;
_serverRoleAccessor = serverRoleAccessor;
_deliveryApiSettings = deliveryApiSettings.Value;
}
@@ -40,13 +47,34 @@ internal sealed class InitializeMemberApplicationNotificationHandler : INotifica
return;
}
// we cannot inject the IMemberApplicationManager because it ultimately takes a dependency on the DbContext ... and during
// install that is not allowed (no connection string means no DbContext)
using IServiceScope scope = _serviceScopeFactory.CreateScope();
IMemberApplicationManager memberApplicationManager = scope.ServiceProvider.GetRequiredService<IMemberApplicationManager>();
if (_serverRoleAccessor.CurrentServerRole is ServerRole.Subscriber)
{
// subscriber instances should not alter the member application
return;
}
await HandleMemberApplication(memberApplicationManager, cancellationToken);
await HandleMemberClientCredentialsApplication(memberApplicationManager, cancellationToken);
try
{
await _locker.WaitAsync(cancellationToken);
if (_isInitialized)
{
return;
}
// we cannot inject the IMemberApplicationManager because it ultimately takes a dependency on the DbContext ... and during
// install that is not allowed (no connection string means no DbContext)
using IServiceScope scope = _serviceScopeFactory.CreateScope();
IMemberApplicationManager memberApplicationManager = scope.ServiceProvider.GetRequiredService<IMemberApplicationManager>();
await HandleMemberApplication(memberApplicationManager, cancellationToken);
await HandleMemberClientCredentialsApplication(memberApplicationManager, cancellationToken);
_isInitialized = true;
}
finally
{
_locker.Release();
}
}
private async Task HandleMemberApplication(IMemberApplicationManager memberApplicationManager, CancellationToken cancellationToken)
@@ -124,7 +124,7 @@ internal sealed class RequestContextOutputExpansionStrategyV2 : IOutputExpansion
private object? GetPropertyValue(IPublishedProperty property)
=> _propertyRenderer.GetPropertyValue(property, _expandProperties.Peek() is not null);
private class Node
private sealed class Node
{
public string Key { get; private set; } = string.Empty;
@@ -1,5 +1,6 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Api.Common.ViewModels.Pagination;
using Umbraco.Cms.Api.Management.ViewModels.Tree;
using Umbraco.Cms.Core.Services;
@@ -13,7 +14,10 @@ public class SiblingsDataTypeTreeController : DataTypeTreeControllerBase
}
[HttpGet("siblings")]
[ProducesResponseType(typeof(IEnumerable<DataTypeTreeItemResponseModel>), StatusCodes.Status200OK)]
public Task<ActionResult<IEnumerable<DataTypeTreeItemResponseModel>>> Siblings(CancellationToken cancellationToken, Guid target, int before, int after)
=> GetSiblings(target, before, after);
[ProducesResponseType(typeof(SubsetViewModel<DataTypeTreeItemResponseModel>), StatusCodes.Status200OK)]
public async Task<ActionResult<SubsetViewModel<DataTypeTreeItemResponseModel>>> Siblings(CancellationToken cancellationToken, Guid target, int before, int after, bool foldersOnly = false)
{
RenderFoldersOnly(foldersOnly);
return await GetSiblings(target, before, after);
}
}
@@ -1,11 +1,14 @@
using Asp.Versioning;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.DependencyInjection;
using Umbraco.Cms.Api.Management.Factories;
using Umbraco.Cms.Api.Management.ViewModels.Document.Item;
using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.Entities;
using Umbraco.Cms.Core.Services;
using Umbraco.Extensions;
namespace Umbraco.Cms.Api.Management.Controllers.Document.Item;
@@ -14,13 +17,52 @@ public class SearchDocumentItemController : DocumentItemControllerBase
{
private readonly IIndexedEntitySearchService _indexedEntitySearchService;
private readonly IDocumentPresentationFactory _documentPresentationFactory;
private readonly IDataTypeService _dataTypeService;
public SearchDocumentItemController(IIndexedEntitySearchService indexedEntitySearchService, IDocumentPresentationFactory documentPresentationFactory)
[ActivatorUtilitiesConstructor]
public SearchDocumentItemController(
IIndexedEntitySearchService indexedEntitySearchService,
IDocumentPresentationFactory documentPresentationFactory,
IDataTypeService dataTypeService)
{
_indexedEntitySearchService = indexedEntitySearchService;
_documentPresentationFactory = documentPresentationFactory;
_dataTypeService = dataTypeService;
}
[Obsolete("Use the non-obsolete constructor instead, will be removed in v18")]
public SearchDocumentItemController(
IIndexedEntitySearchService indexedEntitySearchService,
IDocumentPresentationFactory documentPresentationFactory)
: this(
indexedEntitySearchService,
documentPresentationFactory,
StaticServiceProvider.Instance.GetRequiredService<IDataTypeService>())
{
}
[Obsolete("Please use the overload taking all parameters. Scheduled for removal in Umbraco 18.")]
[ApiExplorerSettings(IgnoreApi = true)]
public async Task<IActionResult> SearchWithTrashed(
CancellationToken cancellationToken,
string query,
bool? trashed = null,
string? culture = null,
int skip = 0,
int take = 100,
Guid? parentId = null,
[FromQuery] IEnumerable<Guid>? allowedDocumentTypes = null)
=> await SearchWithTrashed(
cancellationToken,
query,
trashed,
culture,
skip,
take,
parentId,
allowedDocumentTypes,
null);
[HttpGet("search")]
[MapToApiVersion("1.0")]
[ProducesResponseType(typeof(PagedModel<DocumentItemResponseModel>), StatusCodes.Status200OK)]
@@ -32,9 +74,21 @@ public class SearchDocumentItemController : DocumentItemControllerBase
int skip = 0,
int take = 100,
Guid? parentId = null,
[FromQuery] IEnumerable<Guid>? allowedDocumentTypes = null)
[FromQuery] IEnumerable<Guid>? allowedDocumentTypes = null,
Guid? dataTypeId = null)
{
PagedModel<IEntitySlim> searchResult = await _indexedEntitySearchService.SearchAsync(UmbracoObjectTypes.Document, query, parentId, allowedDocumentTypes, trashed, culture, skip, take);
var ignoreUserStartNodes = await IgnoreUserStartNodes(dataTypeId);
PagedModel<IEntitySlim> searchResult = await _indexedEntitySearchService.SearchAsync(
UmbracoObjectTypes.Document,
query,
parentId,
allowedDocumentTypes,
trashed,
culture,
skip,
take,
ignoreUserStartNodes);
var result = new PagedModel<DocumentItemResponseModel>
{
Items = searchResult.Items.OfType<IDocumentEntitySlim>().Select(_documentPresentationFactory.CreateItemResponseModel),
@@ -43,4 +97,7 @@ public class SearchDocumentItemController : DocumentItemControllerBase
return Ok(result);
}
private async Task<bool> IgnoreUserStartNodes(Guid? dataTypeKey) =>
dataTypeKey is not null && await _dataTypeService.IsDataTypeIgnoringUserStartNodesAsync(dataTypeKey.Value);
}
@@ -58,11 +58,12 @@ public class PublishDocumentWithDescendantsController : DocumentControllerBase
true);
return attempt.Success && attempt.Result.AcceptedTaskId.HasValue
? Ok(new PublishWithDescendantsResultModel
{
TaskId = attempt.Result.AcceptedTaskId.Value,
IsComplete = false
})
? Ok(
new PublishWithDescendantsResultModel
{
TaskId = attempt.Result.AcceptedTaskId.Value,
IsComplete = false,
})
: DocumentPublishingOperationStatusResult(attempt.Status, failedBranchItems: attempt.Result.FailedItems);
}
@@ -49,21 +49,23 @@ public class PublishDocumentWithDescendantsResultController : DocumentController
var isPublishing = await _contentPublishingService.IsPublishingBranchAsync(taskId);
if (isPublishing)
{
return Ok(new PublishWithDescendantsResultModel
{
TaskId = taskId,
IsComplete = false
});
};
return Ok(
new PublishWithDescendantsResultModel
{
TaskId = taskId,
IsComplete = false,
});
}
// If completed, get the result and return the status.
Attempt<ContentPublishingBranchResult, ContentPublishingOperationStatus> attempt = await _contentPublishingService.GetPublishBranchResultAsync(taskId);
return attempt.Success
? Ok(new PublishWithDescendantsResultModel
{
TaskId = taskId,
IsComplete = true
})
? Ok(
new PublishWithDescendantsResultModel
{
TaskId = taskId,
IsComplete = true,
})
: DocumentPublishingOperationStatusResult(attempt.Status, failedBranchItems: attempt.Result.FailedItems);
}
}
@@ -36,7 +36,12 @@ public class ChildrenDocumentTreeController : DocumentTreeControllerBase
[HttpGet("children")]
[MapToApiVersion("1.0")]
[ProducesResponseType(typeof(PagedViewModel<DocumentTreeItemResponseModel>), StatusCodes.Status200OK)]
public async Task<ActionResult<PagedViewModel<DocumentTreeItemResponseModel>>> Children(CancellationToken cancellationToken, Guid parentId, int skip = 0, int take = 100, Guid? dataTypeId = null)
public async Task<ActionResult<PagedViewModel<DocumentTreeItemResponseModel>>> Children(
CancellationToken cancellationToken,
Guid parentId,
int skip = 0,
int take = 100,
Guid? dataTypeId = null)
{
IgnoreUserStartNodesForDataType(dataTypeId);
return await GetChildren(parentId, skip, take);
@@ -1,6 +1,7 @@
using Asp.Versioning;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Api.Common.ViewModels.Pagination;
using Umbraco.Cms.Api.Management.Factories;
using Umbraco.Cms.Api.Management.Services.Entities;
using Umbraco.Cms.Api.Management.ViewModels.Tree;
@@ -34,7 +35,10 @@ public class SiblingsDocumentTreeController : DocumentTreeControllerBase
[HttpGet("siblings")]
[MapToApiVersion("1.0")]
[ProducesResponseType(typeof(IEnumerable<DocumentTreeItemResponseModel>), StatusCodes.Status200OK)]
public Task<ActionResult<IEnumerable<DocumentTreeItemResponseModel>>> Siblings(CancellationToken cancellationToken, Guid target, int before, int after)
=> GetSiblings(target, before, after);
[ProducesResponseType(typeof(SubsetViewModel<DocumentTreeItemResponseModel>), StatusCodes.Status200OK)]
public async Task<ActionResult<SubsetViewModel<DocumentTreeItemResponseModel>>> Siblings(CancellationToken cancellationToken, Guid target, int before, int after, Guid? dataTypeId = null)
{
IgnoreUserStartNodesForDataType(dataTypeId);
return await GetSiblings(target, before, after);
}
}
@@ -1,5 +1,6 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Api.Common.ViewModels.Pagination;
using Umbraco.Cms.Api.Management.Factories;
using Umbraco.Cms.Api.Management.ViewModels.Tree;
using Umbraco.Cms.Core.Services;
@@ -14,11 +15,15 @@ public class SiblingsDocumentBlueprintTreeController : DocumentBlueprintTreeCont
}
[HttpGet("siblings")]
[ProducesResponseType(typeof(IEnumerable<DocumentBlueprintTreeItemResponseModel>), StatusCodes.Status200OK)]
public Task<ActionResult<IEnumerable<DocumentBlueprintTreeItemResponseModel>>> Siblings(
[ProducesResponseType(typeof(SubsetViewModel<DocumentBlueprintTreeItemResponseModel>), StatusCodes.Status200OK)]
public async Task<ActionResult<SubsetViewModel<DocumentBlueprintTreeItemResponseModel>>> Siblings(
CancellationToken cancellationToken,
Guid target,
int before,
int after) =>
GetSiblings(target, before, after);
int after,
bool foldersOnly = false)
{
RenderFoldersOnly(foldersOnly);
return await GetSiblings(target, before, after);
}
}
@@ -1,5 +1,6 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Api.Common.ViewModels.Pagination;
using Umbraco.Cms.Api.Management.ViewModels.Tree;
using Umbraco.Cms.Core.Services;
@@ -13,11 +14,15 @@ public class SiblingsDocumentTypeTreeController : DocumentTypeTreeControllerBase
}
[HttpGet("siblings")]
[ProducesResponseType(typeof(IEnumerable<DocumentTypeTreeItemResponseModel>), StatusCodes.Status200OK)]
public Task<ActionResult<IEnumerable<DocumentTypeTreeItemResponseModel>>> Siblings(
[ProducesResponseType(typeof(SubsetViewModel<DocumentTypeTreeItemResponseModel>), StatusCodes.Status200OK)]
public async Task<ActionResult<SubsetViewModel<DocumentTypeTreeItemResponseModel>>> Siblings(
CancellationToken cancellationToken,
Guid target,
int before,
int after) =>
GetSiblings(target, before, after);
int after,
bool foldersOnly = false)
{
RenderFoldersOnly(foldersOnly);
return await GetSiblings(target, before, after);
}
}
@@ -1,11 +1,14 @@
using Asp.Versioning;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.DependencyInjection;
using Umbraco.Cms.Api.Management.Factories;
using Umbraco.Cms.Api.Management.ViewModels.Media.Item;
using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.Entities;
using Umbraco.Cms.Core.Services;
using Umbraco.Extensions;
namespace Umbraco.Cms.Api.Management.Controllers.Media.Item;
@@ -14,13 +17,52 @@ public class SearchMediaItemController : MediaItemControllerBase
{
private readonly IIndexedEntitySearchService _indexedEntitySearchService;
private readonly IMediaPresentationFactory _mediaPresentationFactory;
private readonly IDataTypeService _dataTypeService;
public SearchMediaItemController(IIndexedEntitySearchService indexedEntitySearchService, IMediaPresentationFactory mediaPresentationFactory)
[ActivatorUtilitiesConstructor]
public SearchMediaItemController(
IIndexedEntitySearchService indexedEntitySearchService,
IMediaPresentationFactory mediaPresentationFactory,
IDataTypeService dataTypeService)
{
_indexedEntitySearchService = indexedEntitySearchService;
_mediaPresentationFactory = mediaPresentationFactory;
_dataTypeService = dataTypeService;
}
[Obsolete("Use the non-obsolete constructor instead, will be removed in Umbraco 18.")]
public SearchMediaItemController(
IIndexedEntitySearchService indexedEntitySearchService,
IMediaPresentationFactory mediaPresentationFactory)
: this(
indexedEntitySearchService,
mediaPresentationFactory,
StaticServiceProvider.Instance.GetRequiredService<IDataTypeService>())
{
}
[Obsolete("Please use the overload taking all parameters. Scheduled for removal in Umbraco 18.")]
[ApiExplorerSettings(IgnoreApi = true)]
public async Task<IActionResult> SearchFromParentWithAllowedTypes(
CancellationToken cancellationToken,
string query,
bool? trashed = null,
string? culture = null,
int skip = 0,
int take = 100,
Guid? parentId = null,
[FromQuery] IEnumerable<Guid>? allowedMediaTypes = null)
=> await SearchFromParentWithAllowedTypes(
cancellationToken,
query,
trashed,
culture,
skip,
take,
parentId,
allowedMediaTypes,
null);
[HttpGet("search")]
[MapToApiVersion("1.0")]
[ProducesResponseType(typeof(PagedModel<MediaItemResponseModel>), StatusCodes.Status200OK)]
@@ -32,9 +74,20 @@ public class SearchMediaItemController : MediaItemControllerBase
int skip = 0,
int take = 100,
Guid? parentId = null,
[FromQuery]IEnumerable<Guid>? allowedMediaTypes = null)
[FromQuery] IEnumerable<Guid>? allowedMediaTypes = null,
Guid? dataTypeId = null)
{
PagedModel<IEntitySlim> searchResult = await _indexedEntitySearchService.SearchAsync(UmbracoObjectTypes.Media, query, parentId, allowedMediaTypes, trashed, culture, skip, take);
var ignoreUserStartNodes = await IgnoreUserStartNodes(dataTypeId);
PagedModel<IEntitySlim> searchResult = await _indexedEntitySearchService.SearchAsync(
UmbracoObjectTypes.Media,
query,
parentId,
allowedMediaTypes,
trashed,
culture,
skip,
take,
ignoreUserStartNodes);
var result = new PagedModel<MediaItemResponseModel>
{
Items = searchResult.Items.OfType<IMediaEntitySlim>().Select(_mediaPresentationFactory.CreateItemResponseModel),
@@ -43,4 +96,7 @@ public class SearchMediaItemController : MediaItemControllerBase
return Ok(result);
}
private async Task<bool> IgnoreUserStartNodes(Guid? dataTypeKey) =>
dataTypeKey is not null && await _dataTypeService.IsDataTypeIgnoringUserStartNodesAsync(dataTypeKey.Value);
}
@@ -1,5 +1,6 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Api.Common.ViewModels.Pagination;
using Umbraco.Cms.Api.Management.Factories;
using Umbraco.Cms.Api.Management.Services.Entities;
using Umbraco.Cms.Api.Management.ViewModels.Tree;
@@ -23,7 +24,10 @@ public class SiblingsMediaTreeController : MediaTreeControllerBase
}
[HttpGet("siblings")]
[ProducesResponseType(typeof(IEnumerable<MediaTreeItemResponseModel>), StatusCodes.Status200OK)]
public Task<ActionResult<IEnumerable<MediaTreeItemResponseModel>>> Siblings(CancellationToken cancellationToken, Guid target, int before, int after)
=> GetSiblings(target, before, after);
[ProducesResponseType(typeof(SubsetViewModel<MediaTreeItemResponseModel>), StatusCodes.Status200OK)]
public async Task<ActionResult<SubsetViewModel<MediaTreeItemResponseModel>>> Siblings(CancellationToken cancellationToken, Guid target, int before, int after, Guid? dataTypeId = null)
{
IgnoreUserStartNodesForDataType(dataTypeId);
return await GetSiblings(target, before, after);
}
}
@@ -1,5 +1,6 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Api.Common.ViewModels.Pagination;
using Umbraco.Cms.Api.Management.ViewModels.Tree;
using Umbraco.Cms.Core.Services;
@@ -13,7 +14,15 @@ public class SiblingsMediaTypeTreeController : MediaTypeTreeControllerBase
}
[HttpGet("siblings")]
[ProducesResponseType(typeof(IEnumerable<MediaTypeTreeItemResponseModel>), StatusCodes.Status200OK)]
public Task<ActionResult<IEnumerable<MediaTypeTreeItemResponseModel>>> Siblings(CancellationToken cancellationToken, Guid target, int before, int after)
=> GetSiblings(target, before, after);
[ProducesResponseType(typeof(SubsetViewModel<MediaTypeTreeItemResponseModel>), StatusCodes.Status200OK)]
public async Task<ActionResult<SubsetViewModel<MediaTypeTreeItemResponseModel>>> Siblings(
CancellationToken cancellationToken,
Guid target,
int before,
int after,
bool foldersOnly = false)
{
RenderFoldersOnly(foldersOnly);
return await GetSiblings(target, before, after);
}
}
@@ -1,6 +1,8 @@
using Asp.Versioning;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.PublishedCache;
namespace Umbraco.Cms.Api.Management.Controllers.PublishedCache;
@@ -15,9 +17,10 @@ public class RebuildPublishedCacheController : PublishedCacheControllerBase
[HttpPost("rebuild")]
[MapToApiVersion("1.0")]
[ProducesResponseType(StatusCodes.Status200OK)]
public Task<IActionResult> Rebuild(CancellationToken cancellationToken)
public async Task<IActionResult> Rebuild(CancellationToken cancellationToken)
{
if (_databaseCacheRebuilder.IsRebuilding())
Attempt<DatabaseCacheRebuildResult> attempt = await _databaseCacheRebuilder.RebuildAsync(true);
if (attempt is { Success: false, Result: DatabaseCacheRebuildResult.AlreadyRunning })
{
var problemDetails = new ProblemDetails
{
@@ -26,11 +29,9 @@ public class RebuildPublishedCacheController : PublishedCacheControllerBase
Status = StatusCodes.Status400BadRequest,
Type = "Error",
};
return Task.FromResult<IActionResult>(Conflict(problemDetails));
return Conflict(problemDetails);
}
_databaseCacheRebuilder.Rebuild(true);
return Task.FromResult<IActionResult>(Ok());
return Ok();
}
}
@@ -16,12 +16,13 @@ public class RebuildPublishedCacheStatusController : PublishedCacheControllerBas
[HttpGet("rebuild/status")]
[MapToApiVersion("1.0")]
[ProducesResponseType(typeof(RebuildStatusModel), StatusCodes.Status200OK)]
public Task<IActionResult> Status(CancellationToken cancellationToken)
public async Task<IActionResult> Status(CancellationToken cancellationToken)
{
var isRebuilding = _databaseCacheRebuilder.IsRebuilding();
return Task.FromResult((IActionResult)Ok(new RebuildStatusModel
{
IsRebuilding = isRebuilding
}));
var isRebuilding = await _databaseCacheRebuilder.IsRebuildingAsync();
return Ok(
new RebuildStatusModel
{
IsRebuilding = isRebuilding,
});
}
}
@@ -1,5 +1,6 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Api.Common.ViewModels.Pagination;
using Umbraco.Cms.Api.Management.ViewModels.Tree;
using Umbraco.Cms.Core.Services;
@@ -13,11 +14,11 @@ public class SiblingsTemplateTreeController : TemplateTreeControllerBase
}
[HttpGet("siblings")]
[ProducesResponseType(typeof(IEnumerable<NamedEntityTreeItemResponseModel>), StatusCodes.Status200OK)]
public Task<ActionResult<IEnumerable<NamedEntityTreeItemResponseModel>>> Siblings(
[ProducesResponseType(typeof(SubsetViewModel<NamedEntityTreeItemResponseModel>), StatusCodes.Status200OK)]
public async Task<ActionResult<SubsetViewModel<NamedEntityTreeItemResponseModel>>> Siblings(
CancellationToken cancellationToken,
Guid target,
int before,
int after) =>
GetSiblings(target, before, after);
await GetSiblings(target, before, after);
}
@@ -1,4 +1,4 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Api.Common.ViewModels.Pagination;
using Umbraco.Cms.Api.Management.ViewModels;
using Umbraco.Cms.Api.Management.ViewModels.Tree;
@@ -44,12 +44,12 @@ public abstract class EntityTreeControllerBase<TItem> : ManagementApiControllerB
return Task.FromResult<ActionResult<PagedViewModel<TItem>>>(Ok(result));
}
protected Task<ActionResult<IEnumerable<TItem>>> GetSiblings(Guid target, int before, int after)
protected Task<ActionResult<SubsetViewModel<TItem>>> GetSiblings(Guid target, int before, int after)
{
IEntitySlim[] siblings = EntityService.GetSiblings(target, ItemObjectType, before, after, ItemOrdering).ToArray();
IEntitySlim[] siblings = GetSiblingEntities(target, before, after, out var totalBefore, out var totalAfter);
if (siblings.Length == 0)
{
return Task.FromResult<ActionResult<IEnumerable<TItem>>>(NotFound());
return Task.FromResult<ActionResult<SubsetViewModel<TItem>>>(NotFound());
}
IEntitySlim? entity = siblings.FirstOrDefault();
@@ -57,8 +57,11 @@ public abstract class EntityTreeControllerBase<TItem> : ManagementApiControllerB
? EntityService.GetKey(entity.ParentId, ItemObjectType).Result
: Constants.System.RootKey;
TItem[] treeItemsViewModels = MapTreeItemViewModels(parentKey, siblings);
return Task.FromResult<ActionResult<IEnumerable<TItem>>>(Ok(treeItemsViewModels));
TItem[] treeItemViewModels = MapTreeItemViewModels(parentKey, siblings);
SubsetViewModel<TItem> result = SubsetViewModel(treeItemViewModels, totalBefore, totalAfter);
return Task.FromResult<ActionResult<SubsetViewModel<TItem>>>(Ok(result));
}
protected virtual async Task<ActionResult<IEnumerable<TItem>>> GetAncestors(Guid descendantKey, bool includeSelf = true)
@@ -110,7 +113,8 @@ public abstract class EntityTreeControllerBase<TItem> : ManagementApiControllerB
.ToArray();
protected virtual IEntitySlim[] GetPagedChildEntities(Guid parentKey, int skip, int take, out long totalItems) =>
EntityService.GetPagedChildren(
EntityService
.GetPagedChildren(
parentKey,
ItemObjectType,
skip,
@@ -119,6 +123,18 @@ public abstract class EntityTreeControllerBase<TItem> : ManagementApiControllerB
ordering: ItemOrdering)
.ToArray();
protected virtual IEntitySlim[] GetSiblingEntities(Guid target, int before, int after, out long totalBefore, out long totalAfter) =>
EntityService
.GetSiblings(
target,
[ItemObjectType],
before,
after,
out totalBefore,
out totalAfter,
ordering: ItemOrdering)
.ToArray();
protected virtual TItem[] MapTreeItemViewModels(Guid? parentKey, IEntitySlim[] entities)
=> entities.Select(entity => MapTreeItemViewModel(parentKey, entity)).ToArray();
@@ -141,4 +157,7 @@ public abstract class EntityTreeControllerBase<TItem> : ManagementApiControllerB
protected PagedViewModel<TItem> PagedViewModel(IEnumerable<TItem> treeItemViewModels, long totalItems)
=> new() { Total = totalItems, Items = treeItemViewModels };
protected SubsetViewModel<TItem> SubsetViewModel(IEnumerable<TItem> treeItemViewModels, long totalBefore, long totalAfter)
=> new() { TotalBefore = totalBefore, TotalAfter = totalAfter, Items = treeItemViewModels };
}
@@ -51,6 +51,24 @@ public abstract class FolderTreeControllerBase<TItem> : NamedEntityTreeControlle
take,
out totalItems);
protected override IEntitySlim[] GetSiblingEntities(Guid target, int before, int after, out long totalBefore, out long totalAfter)
{
totalBefore = 0;
totalAfter = 0;
UmbracoObjectTypes[] siblingObjectTypes = GetObjectTypes();
return EntityService.GetSiblings(
target,
siblingObjectTypes,
before,
after,
out totalBefore,
out totalAfter,
ordering: ItemOrdering)
.ToArray();
}
protected override TItem MapTreeItemViewModel(Guid? parentKey, IEntitySlim entity)
{
TItem viewModel = base.MapTreeItemViewModel(parentKey, entity);
@@ -93,19 +111,19 @@ public abstract class FolderTreeControllerBase<TItem> : NamedEntityTreeControlle
{
totalItems = 0;
UmbracoObjectTypes[] childObjectTypes = _foldersOnly ? [FolderObjectType] : [FolderObjectType, ItemObjectType];
UmbracoObjectTypes[] childObjectTypes = GetObjectTypes();
IEntitySlim[] itemEntities = EntityService.GetPagedChildren(
parentKey,
[FolderObjectType, ItemObjectType],
childObjectTypes,
skip,
take,
false,
out totalItems,
ordering: ItemOrdering)
.ToArray();
return itemEntities;
return EntityService.GetPagedChildren(
parentKey,
[FolderObjectType, ItemObjectType],
childObjectTypes,
skip,
take,
false,
out totalItems,
ordering: ItemOrdering)
.ToArray();
}
private UmbracoObjectTypes[] GetObjectTypes() => _foldersOnly ? [FolderObjectType] : [FolderObjectType, ItemObjectType];
}
@@ -1,4 +1,4 @@
using Umbraco.Cms.Api.Management.Models.Entities;
using Umbraco.Cms.Api.Management.Models.Entities;
using Umbraco.Cms.Api.Management.Services.Entities;
using Umbraco.Cms.Api.Management.ViewModels.Tree;
using Umbraco.Cms.Core;
@@ -59,6 +59,26 @@ public abstract class UserStartNodeTreeControllerBase<TItem> : EntityTreeControl
return CalculateAccessMap(() => userAccessEntities, out _);
}
protected override IEntitySlim[] GetSiblingEntities(Guid target, int before, int after, out long totalBefore, out long totalAfter)
{
if (UserHasRootAccess() || IgnoreUserStartNodes())
{
return base.GetSiblingEntities(target, before, after, out totalBefore, out totalAfter);
}
IEnumerable<UserAccessEntity> userAccessEntities = _userStartNodeEntitiesService.SiblingUserAccessEntities(
ItemObjectType,
UserStartNodePaths,
target,
before,
after,
ItemOrdering,
out totalBefore,
out totalAfter);
return CalculateAccessMap(() => userAccessEntities, out _);
}
protected override TItem[] MapTreeItemViewModels(Guid? parentKey, IEntitySlim[] entities)
{
if (UserHasRootAccess() || IgnoreUserStartNodes())
@@ -79,7 +79,7 @@ public static class BackOfficeAuthBuilderExtensions
}
}
internal class BackofficePipelineFilter : UmbracoPipelineFilter
internal sealed class BackofficePipelineFilter : UmbracoPipelineFilter
{
public BackofficePipelineFilter(string name)
: base(name)
@@ -4,7 +4,7 @@ namespace Umbraco.Cms.Api.Management.DependencyInjection;
internal static class ManagementApiConfiguration
{
internal const string ApiSecurityName = "Backoffice User";
internal const string ApiSecurityName = "Backoffice-User";
internal const string ApiTitle = "Umbraco Management API";
internal const string ApiName = "management";
@@ -13,7 +13,7 @@ using Umbraco.Extensions;
namespace Umbraco.Cms.Api.Management.Factories;
internal class PackagePresentationFactory : IPackagePresentationFactory
internal sealed class PackagePresentationFactory : IPackagePresentationFactory
{
private readonly IUmbracoMapper _umbracoMapper;
private readonly IRuntimeState _runtimeState;
@@ -8,7 +8,7 @@ using Umbraco.Cms.Core.Webhooks;
namespace Umbraco.Cms.Api.Management.Factories;
internal class WebhookPresentationFactory : IWebhookPresentationFactory
internal sealed class WebhookPresentationFactory : IWebhookPresentationFactory
{
private readonly WebhookEventCollection _webhookEventCollection;
private readonly IHostingEnvironment _hostingEnvironment;
@@ -21,7 +21,7 @@ public sealed class AppendEventMessagesAttribute : TypeFilterAttribute
{
}
private class AppendEventMessagesFilter : IActionFilter
private sealed class AppendEventMessagesFilter : IActionFilter
{
private readonly IEventMessagesFactory _eventMessagesFactory;
private readonly IJsonSerializer _jsonSerializer;
@@ -4,14 +4,14 @@ using Umbraco.Cms.Core.Configuration.Models;
namespace Umbraco.Cms.Api.Management.Filters;
internal class UserPasswordEnsureMinimumResponseTimeAttribute : TypeFilterAttribute
internal sealed class UserPasswordEnsureMinimumResponseTimeAttribute : TypeFilterAttribute
{
public UserPasswordEnsureMinimumResponseTimeAttribute()
: base(typeof(UserPasswordEnsureMinimumResponseTimeFilter))
{
}
private class UserPasswordEnsureMinimumResponseTimeFilter : EnsureMinimumResponseTimeFilter
private sealed class UserPasswordEnsureMinimumResponseTimeFilter : EnsureMinimumResponseTimeFilter
{
public UserPasswordEnsureMinimumResponseTimeFilter(IOptions<UserPasswordConfigurationSettings> options)
: base(options.Value.MinimumResponseTime)
@@ -45,7 +45,7 @@ public static class SectionMapper
return name;
}
private class SectionMapping
private sealed class SectionMapping
{
public required string Alias { get; init; }
File diff suppressed because it is too large Load Diff
@@ -2,7 +2,7 @@ using Umbraco.Cms.Api.Management.DependencyInjection;
namespace Umbraco.Cms.Api.Management.OpenApi;
internal class BackOfficeSecurityRequirementsOperationFilter : BackOfficeSecurityRequirementsOperationFilterBase
internal sealed class BackOfficeSecurityRequirementsOperationFilter : BackOfficeSecurityRequirementsOperationFilterBase
{
protected override string ApiName => ManagementApiConfiguration.ApiName;
}
@@ -6,7 +6,7 @@ using Umbraco.Cms.Core;
namespace Umbraco.Cms.Api.Management.OpenApi;
internal class NotificationHeaderFilter : IOperationFilter
internal sealed class NotificationHeaderFilter : IOperationFilter
{
public void Apply(OpenApiOperation operation, OperationFilterContext context)
{
@@ -7,7 +7,7 @@ using Umbraco.Extensions;
namespace Umbraco.Cms.Api.Management.OpenApi;
internal class ResponseHeaderOperationFilter : IOperationFilter
internal sealed class ResponseHeaderOperationFilter : IOperationFilter
{
public void Apply(OpenApiOperation operation, OperationFilterContext context)
{
@@ -29,7 +29,7 @@ internal class ResponseHeaderOperationFilter : IOperationFilter
}
}
private void SetHeader(OpenApiResponse value, string headerName, string description, string type, string? format = null)
private static void SetHeader(OpenApiResponse value, string headerName, string description, string type, string? format = null)
{
if (value.Headers is null)
@@ -61,7 +61,7 @@ public class BackOfficeAuthenticationBuilder : AuthenticationBuilder
// TODO: We could override and throw NotImplementedException for other methods?
// Ensures that the sign in scheme is always the Umbraco back office external type
internal class EnsureBackOfficeScheme<TOptions> : IPostConfigureOptions<TOptions>
internal sealed class EnsureBackOfficeScheme<TOptions> : IPostConfigureOptions<TOptions>
where TOptions : RemoteAuthenticationOptions
{
public void PostConfigure(string? name, TOptions options)
@@ -7,7 +7,7 @@ namespace Umbraco.Cms.Api.Management.Security;
/// <summary>
/// Custom secure format that ensures the Identity in the ticket is verified <see cref="ClaimsIdentity" />
/// </summary>
internal class BackOfficeSecureDataFormat : ISecureDataFormat<AuthenticationTicket>
internal sealed class BackOfficeSecureDataFormat : ISecureDataFormat<AuthenticationTicket>
{
private readonly TimeSpan _loginTimeout;
private readonly ISecureDataFormat<AuthenticationTicket> _ticketDataFormat;
@@ -7,7 +7,7 @@ internal sealed class UserConnectionManager : IUserConnectionManager
{
// We use a normal dictionary instead of ConcurrentDictionary, since we need to lock the set anyways.
private readonly Dictionary<Guid, HashSet<string>> _connections = new();
private readonly object _lock = new();
private readonly Lock _lock = new();
/// <inheritdoc/>
public ISet<string> GetConnections(Guid userKey)
@@ -1,4 +1,4 @@
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.Entities;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Api.Management.Models.Entities;
@@ -64,6 +64,37 @@ public interface IUserStartNodeEntitiesService
/// </remarks>
IEnumerable<UserAccessEntity> ChildUserAccessEntities(IEnumerable<IEntitySlim> candidateChildren, string[] userStartNodePaths);
/// <summary>
/// Calculates the applicable sibling entities for a given object type for users without root access.
/// </summary>
/// <param name="umbracoObjectType">The object type.</param>
/// <param name="userStartNodePaths">The calculated start node paths for the user.</param>
/// <param name="targetKey">The key of the target.</param>
/// <param name="before">The number of applicable siblings to retrieve before the target.</param>
/// <param name="after">The number of applicable siblings to retrieve after the target.</param>
/// <param name="ordering">The ordering to apply when fetching and paginating the children.</param>
/// <param name="totalBefore">Outputs the total number of siblings before the target entity.</param>
/// <param name="totalAfter">Outputs the total number of siblings after the target entity.</param>
/// <returns>A list of sibling entities applicable for the user.</returns>
/// <remarks>
/// The returned entities may include entities that outside of the user start node scope, but are needed to
/// for browsing to the actual user start nodes. These entities will be marked as "no access" entities.
/// </remarks>
IEnumerable<UserAccessEntity> SiblingUserAccessEntities(
UmbracoObjectTypes umbracoObjectType,
string[] userStartNodePaths,
Guid targetKey,
int before,
int after,
Ordering ordering,
out long totalBefore,
out long totalAfter)
{
totalBefore = 0;
totalAfter = 0;
return [];
}
/// <summary>
/// Calculates the access level of a collection of entities for users without root access.
/// </summary>
@@ -1,4 +1,4 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.Entities;
@@ -36,17 +36,17 @@ public class UserStartNodeEntitiesService : IUserStartNodeEntitiesService
/// <inheritdoc />
public IEnumerable<UserAccessEntity> RootUserAccessEntities(UmbracoObjectTypes umbracoObjectType, int[] userStartNodeIds)
{
// root entities for users without root access should include:
// Root entities for users without root access should include:
// - the start nodes that are actual root entities (level == 1)
// - the root level ancestors to the rest of the start nodes (required for browsing to the actual start nodes - will be marked as "no access")
IEntitySlim[] userStartEntities = userStartNodeIds.Any()
? _entityService.GetAll(umbracoObjectType, userStartNodeIds).ToArray()
: Array.Empty<IEntitySlim>();
// find the start nodes that are at root level (level == 1)
// Find the start nodes that are at root level (level == 1).
IEntitySlim[] allowedTopmostEntities = userStartEntities.Where(entity => entity.Level == 1).ToArray();
// find the root level ancestors of the rest of the start nodes, and add those as well
// Find the root level ancestors of the rest of the start nodes, and add those as well.
var nonAllowedTopmostEntityIds = userStartEntities.Except(allowedTopmostEntities)
.Select(entity => int.TryParse(entity.Path.Split(Constants.CharArrays.Comma).Skip(1).FirstOrDefault(), out var id) ? id : 0)
.Where(id => id > 0)
@@ -63,7 +63,15 @@ public class UserStartNodeEntitiesService : IUserStartNodeEntitiesService
.ToArray();
}
public IEnumerable<UserAccessEntity> ChildUserAccessEntities(UmbracoObjectTypes umbracoObjectType, string[] userStartNodePaths, Guid parentKey, int skip, int take, Ordering ordering, out long totalItems)
/// <inheritdoc/>
public IEnumerable<UserAccessEntity> ChildUserAccessEntities(
UmbracoObjectTypes umbracoObjectType,
string[] userStartNodePaths,
Guid parentKey,
int skip,
int take,
Ordering ordering,
out long totalItems)
{
Attempt<int> parentIdAttempt = _idKeyMap.GetIdForKey(parentKey, umbracoObjectType);
if (parentIdAttempt.Success is false)
@@ -83,40 +91,46 @@ public class UserStartNodeEntitiesService : IUserStartNodeEntitiesService
IEntitySlim[] children;
if (userStartNodePaths.Any(path => $"{parent.Path},".StartsWith($"{path},")))
{
// the requested parent is one of the user start nodes (or a descendant of one), all children are by definition allowed
// The requested parent is one of the user start nodes (or a descendant of one), all children are by definition allowed.
children = _entityService.GetPagedChildren(parentKey, umbracoObjectType, skip, take, out totalItems, ordering: ordering).ToArray();
return ChildUserAccessEntities(children, userStartNodePaths);
}
// if one or more of the user start nodes are descendants of the requested parent, find the "next child IDs" in those user start node paths
// - e.g. given the user start node path "-1,2,3,4,5", if the requested parent ID is 3, the "next child ID" is 4.
var userStartNodePathIds = userStartNodePaths.Select(path => path.Split(Constants.CharArrays.Comma).Select(int.Parse).ToArray()).ToArray();
var allowedChildIds = userStartNodePathIds
.Where(ids => ids.Contains(parentId))
// given the previous checks, the parent ID can never be the last in the user start node path, so this is safe
.Select(ids => ids[ids.IndexOf(parentId) + 1])
.Distinct()
.ToArray();
int[] allowedChildIds = GetAllowedIds(userStartNodePaths, parentId);
totalItems = allowedChildIds.Length;
if (allowedChildIds.Length == 0)
{
// the requested parent is outside the scope of any user start nodes
// The requested parent is outside the scope of any user start nodes.
return [];
}
// even though we know the IDs of the allowed child entities to fetch, we still use a Query to yield correctly sorted children
// Even though we know the IDs of the allowed child entities to fetch, we still use a Query to yield correctly sorted children.
IQuery<IUmbracoEntity> query = _scopeProvider.CreateQuery<IUmbracoEntity>().Where(x => allowedChildIds.Contains(x.Id));
children = _entityService.GetPagedChildren(parentKey, umbracoObjectType, skip, take, out totalItems, query, ordering).ToArray();
return ChildUserAccessEntities(children, userStartNodePaths);
}
private static int[] GetAllowedIds(string[] userStartNodePaths, int parentId)
{
// If one or more of the user start nodes are descendants of the requested parent, find the "next child IDs" in those user start node paths
// that are the final entries in the path.
// E.g. given the user start node path "-1,2,3,4,5", if the requested parent ID is 3, the "next child ID" is 4.
var userStartNodePathIds = userStartNodePaths.Select(path => path.Split(Constants.CharArrays.Comma).Select(int.Parse).ToArray()).ToArray();
return userStartNodePathIds
.Where(ids => ids.Contains(parentId))
.Select(ids => ids[ids.IndexOf(parentId) + 1]) // Given the previous checks, the parent ID can never be the last in the user start node path, so this is safe
.Distinct()
.ToArray();
}
/// <inheritdoc />
public IEnumerable<UserAccessEntity> ChildUserAccessEntities(IEnumerable<IEntitySlim> candidateChildren, string[] userStartNodePaths)
// child entities for users without root access should include:
// Child or sibling entities for users without root access should include:
// - children that are descendant-or-self of a user start node
// - children that are ancestors of a user start node (required for browsing to the actual start nodes - will be marked as "no access")
// all other candidate children should be discarded
// All other candidate children should be discarded.
=> candidateChildren.Select(child =>
{
// is descendant-or-self of a start node?
@@ -134,9 +148,72 @@ public class UserStartNodeEntitiesService : IUserStartNodeEntitiesService
return null;
}).WhereNotNull().ToArray();
/// <inheritdoc />
public IEnumerable<UserAccessEntity> SiblingUserAccessEntities(
UmbracoObjectTypes umbracoObjectType,
string[] userStartNodePaths,
Guid targetKey,
int before,
int after,
Ordering ordering,
out long totalBefore,
out long totalAfter
)
{
Attempt<int> targetIdAttempt = _idKeyMap.GetIdForKey(targetKey, umbracoObjectType);
if (targetIdAttempt.Success is false)
{
totalBefore = 0;
totalAfter = 0;
return [];
}
var targetId = targetIdAttempt.Result;
IEntitySlim? target = _entityService.Get(targetId);
if (target is null)
{
totalBefore = 0;
totalAfter = 0;
return [];
}
IEntitySlim[] siblings;
IEntitySlim? targetParent = _entityService.Get(target.ParentId);
if (targetParent is null) // Even if the parent is the root, we still expect to get a value here.
{
totalBefore = 0;
totalAfter = 0;
return [];
}
if (userStartNodePaths.Any(path => $"{targetParent?.Path},".StartsWith($"{path},")))
{
// The requested parent of the target is one of the user start nodes (or a descendant of one), all siblings are by definition allowed.
siblings = _entityService.GetSiblings(targetKey, [umbracoObjectType], before, after, out totalBefore, out totalAfter, ordering: ordering).ToArray();
return ChildUserAccessEntities(siblings, userStartNodePaths);
}
int[] allowedSiblingIds = GetAllowedIds(userStartNodePaths, targetParent.Id);
if (allowedSiblingIds.Length == 0)
{
// The requested target is outside the scope of any user start nodes.
totalBefore = 0;
totalAfter = 0;
return [];
}
// Even though we know the IDs of the allowed sibling entities to fetch, we still use a Query to yield correctly sorted children.
IQuery<IUmbracoEntity> query = _scopeProvider.CreateQuery<IUmbracoEntity>().Where(x => allowedSiblingIds.Contains(x.Id));
siblings = _entityService.GetSiblings(targetKey, [umbracoObjectType], before, after, out totalBefore, out totalAfter, query, ordering).ToArray();
return ChildUserAccessEntities(siblings, userStartNodePaths);
}
/// <inheritdoc />
public IEnumerable<UserAccessEntity> UserAccessEntities(IEnumerable<IEntitySlim> entities, string[] userStartNodePaths)
// entities for users without root access should include:
// Entities for users without root access should include:
// - entities that are descendant-or-self of a user start node as regular entities
// - all other entities as "no access" entities
=> entities.Select(entity => new UserAccessEntity(entity, IsDescendantOrSelf(entity, userStartNodePaths))).ToArray();
@@ -19,7 +19,7 @@ public class UmbracoEFCoreComposer : IComposer
builder.AddNotificationAsyncHandler<DatabaseSchemaAndDataCreatedNotification, EFCoreCreateTablesNotificationHandler>();
builder.AddNotificationAsyncHandler<UnattendedInstallNotification, EFCoreCreateTablesNotificationHandler>();
builder.Services.AddUmbracoDbContext<UmbracoDbContext>((options) =>
builder.Services.AddUmbracoDbContext<UmbracoDbContext>((provider, options, connectionString, providerName) =>
{
// Register the entity sets needed by OpenIddict.
options.UseOpenIddict();
@@ -17,32 +17,50 @@ public static class UmbracoEFCoreServiceCollectionExtensions
/// <summary>
/// Adds a EFCore DbContext with all the services needed to integrate with Umbraco scopes.
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="services"></param>
/// <param name="optionsAction"></param>
/// <returns></returns>
public static IServiceCollection AddUmbracoDbContext<T>(this IServiceCollection services, Action<DbContextOptionsBuilder>? optionsAction = null)
[Obsolete("Please use the method overload that takes all parameters for the optionsAction. Scheduled for removal in Umbraco 18.")]
public static IServiceCollection AddUmbracoDbContext<T>(
this IServiceCollection services,
Action<DbContextOptionsBuilder>? optionsAction = null)
where T : DbContext
=> AddUmbracoDbContext<T>(services, (sp, optionsBuilder, connectionString, providerName) => optionsAction?.Invoke(optionsBuilder));
/// <summary>
/// Adds a EFCore DbContext with all the services needed to integrate with Umbraco scopes.
/// </summary>
public static IServiceCollection AddUmbracoDbContext<T>(
this IServiceCollection services,
Action<DbContextOptionsBuilder, string?, string?, IServiceProvider?>? optionsAction = null)
where T : DbContext
{
return AddUmbracoDbContext<T>(services, (IServiceProvider _, DbContextOptionsBuilder options) =>
return AddUmbracoDbContext<T>(services, (IServiceProvider provider, DbContextOptionsBuilder optionsBuilder, string? providerName, string? connectionString) =>
{
optionsAction?.Invoke(options);
ConnectionStrings connectionStrings = GetConnectionStringAndProviderName(provider);
optionsAction?.Invoke(optionsBuilder, connectionStrings.ConnectionString, connectionStrings.ProviderName, provider);
});
}
/// <summary>
/// Adds a EFCore DbContext with all the services needed to integrate with Umbraco scopes.
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="services"></param>
/// <param name="optionsAction"></param>
/// <returns></returns>
public static IServiceCollection AddUmbracoDbContext<T>(this IServiceCollection services, Action<IServiceProvider, DbContextOptionsBuilder>? optionsAction = null)
[Obsolete("Please use the method overload that takes all parameters for the optionsAction. Scheduled for removal in Umbraco 18.")]
public static IServiceCollection AddUmbracoDbContext<T>(
this IServiceCollection services,
Action<IServiceProvider, DbContextOptionsBuilder>? optionsAction = null)
where T : DbContext
=> AddUmbracoDbContext<T>(services, (sp, optionsBuilder, connectionString, providerName) => optionsAction?.Invoke(sp, optionsBuilder));
/// <summary>
/// Adds a EFCore DbContext with all the services needed to integrate with Umbraco scopes.
/// </summary>
public static IServiceCollection AddUmbracoDbContext<T>(
this IServiceCollection services,
Action<IServiceProvider, DbContextOptionsBuilder, string?, string?>? optionsAction = null)
where T : DbContext
{
optionsAction ??= (sp, options) => { };
optionsAction ??= (sp, optionsBuilder, connectionString, providerName) => { };
services.AddPooledDbContextFactory<T>(optionsAction);
services.AddPooledDbContextFactory<T>((provider, optionsBuilder) => SetupDbContext(optionsAction, provider, optionsBuilder));
services.AddTransient(services => services.GetRequiredService<IDbContextFactory<T>>().CreateDbContext());
services.AddUnique<IAmbientEFCoreScopeStack<T>, AmbientEFCoreScopeStack<T>>();
@@ -110,4 +128,25 @@ public static class UmbracoEFCoreServiceCollectionExtensions
builder.UseDatabaseProvider(connectionStrings.ProviderName, connectionStrings.ConnectionString);
}
private static void SetupDbContext(Action<IServiceProvider, DbContextOptionsBuilder, string?, string?>? optionsAction, IServiceProvider provider, DbContextOptionsBuilder builder)
{
ConnectionStrings connectionStrings = GetConnectionStringAndProviderName(provider);
optionsAction?.Invoke(provider, builder, connectionStrings.ConnectionString, connectionStrings.ProviderName);
}
private static ConnectionStrings GetConnectionStringAndProviderName(IServiceProvider serviceProvider)
{
ConnectionStrings connectionStrings = serviceProvider.GetRequiredService<IOptionsMonitor<ConnectionStrings>>().CurrentValue;
// Replace data directory
string? dataDirectory = AppDomain.CurrentDomain.GetData(Constants.System.DataDirectoryName)?.ToString();
if (string.IsNullOrEmpty(dataDirectory) is false)
{
connectionStrings.ConnectionString = connectionStrings.ConnectionString?.Replace(Constants.System.DataDirectoryPlaceholder, dataDirectory);
}
return connectionStrings;
}
}
@@ -127,10 +127,16 @@ internal class EFCoreScope<TDbContext> : CoreScope, IEfCoreScope<TDbContext>
Locks.ClearLocks(InstanceId);
if (ParentScope is null)
// Since we can nest EFCoreScopes in other scopes derived from CoreScope, we should check whether our ParentScope OR the base ParentScope exists.
// Only if neither do do we take responsibility for ensuring the locks are cleared.
// Eventually the highest parent will clear the locks.
// Further, these locks are a reference to the locks of the highest parent anyway (see the constructor of CoreScope).
#pragma warning disable SA1100 // Do not prefix calls with base unless local implementation exists (justification: provides additional clarify here that this is defined on the base class).
if (ParentScope is null && base.HasParentScope is false)
{
Locks.EnsureLocksCleared(InstanceId);
}
#pragma warning restore SA1100 // Do not prefix calls with base unless local implementation exists
_efCoreScopeProvider.PopAmbientScope();
@@ -1,4 +1,5 @@
using System.Configuration;
using System.Diagnostics;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.Extensions.DependencyInjection;
@@ -17,14 +18,14 @@ namespace Umbraco.Cms.Persistence.EFCore;
/// and insure the 'src/Umbraco.Web.UI/appsettings.json' have a connection string set with the right provider.
///
/// Create a migration for each provider.
/// <code>dotnet ef migrations add %Name% -s src/Umbraco.Web.UI -p src/Umbraco.Cms.Persistence.EFCore.SqlServer -c UmbracoDbContext -- --provider SqlServer</code>
/// <code>dotnet ef migrations add %Name% -s src/Umbraco.Web.UI -p src/Umbraco.Cms.Persistence.EFCore.SqlServer -c UmbracoDbContext</code>
///
/// <code>dotnet ef migrations add %Name% -s src/Umbraco.Web.UI -p src/Umbraco.Cms.Persistence.EFCore.Sqlite -c UmbracoDbContext -- --provider Sqlite</code>
/// <code>dotnet ef migrations add %Name% -s src/Umbraco.Web.UI -p src/Umbraco.Cms.Persistence.EFCore.Sqlite -c UmbracoDbContext</code>
///
/// Remove the last migration for each provider.
/// <code>dotnet ef migrations remove -s src/Umbraco.Web.UI -p src/Umbraco.Cms.Persistence.EFCore.SqlServer -- --provider SqlServer</code>
/// <code>dotnet ef migrations remove -s src/Umbraco.Web.UI -p src/Umbraco.Cms.Persistence.EFCore.SqlServer</code>
///
/// <code>dotnet ef migrations remove -s src/Umbraco.Web.UI -p src/Umbraco.Cms.Persistence.EFCore.Sqlite -- --provider Sqlite</code>
/// <code>dotnet ef migrations remove -s src/Umbraco.Web.UI -p src/Umbraco.Cms.Persistence.EFCore.Sqlite</code>
///
/// To find documentation about this way of working with the context see
/// https://learn.microsoft.com/en-us/ef/core/managing-schemas/migrations/providers?tabs=dotnet-core-cli#using-one-context-type
@@ -37,28 +38,35 @@ public class UmbracoDbContext : DbContext
/// <param name="options"></param>
public UmbracoDbContext(DbContextOptions<UmbracoDbContext> options)
: base(ConfigureOptions(options))
{
}
{ }
private static DbContextOptions<UmbracoDbContext> ConfigureOptions(DbContextOptions<UmbracoDbContext> options)
{
IOptionsMonitor<ConnectionStrings> connectionStringsOptionsMonitor = StaticServiceProvider.Instance.GetRequiredService<IOptionsMonitor<ConnectionStrings>>();
ConnectionStrings connectionStrings = connectionStringsOptionsMonitor.CurrentValue;
if (string.IsNullOrWhiteSpace(connectionStrings.ConnectionString))
var extensions = options.Extensions.FirstOrDefault() as Microsoft.EntityFrameworkCore.Infrastructure.CoreOptionsExtension;
IServiceProvider? serviceProvider = extensions?.ApplicationServiceProvider;
serviceProvider ??= StaticServiceProvider.Instance;
if (serviceProvider == null)
{
ILogger<UmbracoDbContext> logger = StaticServiceProvider.Instance.GetRequiredService<ILogger<UmbracoDbContext>>();
logger.LogCritical("No connection string was found, cannot setup Umbraco EF Core context");
// If the service provider is null, we cannot resolve the connection string or migration provider.
throw new InvalidOperationException("The service provider is not configured. Ensure that UmbracoDbContext is registered correctly.");
}
IOptionsMonitor<ConnectionStrings>? connectionStringsOptionsMonitor = serviceProvider?.GetRequiredService<IOptionsMonitor<ConnectionStrings>>();
ConnectionStrings? connectionStrings = connectionStringsOptionsMonitor?.CurrentValue;
if (string.IsNullOrWhiteSpace(connectionStrings?.ConnectionString))
{
ILogger<UmbracoDbContext>? logger = serviceProvider?.GetRequiredService<ILogger<UmbracoDbContext>>();
logger?.LogCritical("No connection string was found, cannot setup Umbraco EF Core context");
// we're throwing an exception here to make it abundantly clear that one should never utilize (or have a
// dependency on) the DbContext before the connection string has been initialized by the installer.
throw new InvalidOperationException("No connection string was found, cannot setup Umbraco EF Core context");
}
IEnumerable<IMigrationProviderSetup> migrationProviders = StaticServiceProvider.Instance.GetServices<IMigrationProviderSetup>();
IMigrationProviderSetup? migrationProvider = migrationProviders.FirstOrDefault(x => x.ProviderName.CompareProviderNames(connectionStrings.ProviderName));
IEnumerable<IMigrationProviderSetup>? migrationProviders = serviceProvider?.GetServices<IMigrationProviderSetup>();
IMigrationProviderSetup? migrationProvider = migrationProviders?.FirstOrDefault(x => x.ProviderName.CompareProviderNames(connectionStrings.ProviderName));
if (migrationProvider == null && connectionStrings.ProviderName != null)
{
@@ -162,6 +162,8 @@ public class SqliteSyntaxProvider : SqlSyntaxProviderBase<SqliteSyntaxProvider>
public override string ConvertDateToOrderableString => "{0}";
public override string ConvertUniqueIdentifierToString => "{0}";
public override string RenameTable => "ALTER TABLE {0} RENAME TO {1}";
/// <inheritdoc />
@@ -1,3 +1,5 @@
using System.Text.Json.Serialization;
namespace Umbraco.Cms.Core;
/// <summary>
@@ -9,6 +11,7 @@ namespace Umbraco.Cms.Core;
public struct Attempt<TResult, TStatus>
{
// private - use Succeed() or Fail() methods to create attempts
[JsonConstructor]
private Attempt(bool success, TResult result, TStatus status, Exception? exception)
{
Success = success;
@@ -7,7 +7,7 @@ namespace Umbraco.Cms.Core.Cache.PartialViewCacheInvalidators;
/// The default implementation is added in Umbraco.Web.Website, but we need this to ensure we have a service
/// registered for this interface even in headless setups).
/// </remarks>
internal class NoopMemberPartialViewCacheInvalidator : IMemberPartialViewCacheInvalidator
internal sealed class NoopMemberPartialViewCacheInvalidator : IMemberPartialViewCacheInvalidator
{
/// <inheritdoc/>
public void ClearPartialViewCacheItems(IEnumerable<int> memberIds)
@@ -9,7 +9,7 @@ namespace Umbraco.Cms.Core.Collections;
/// </summary>
/// <typeparam name="T"></typeparam>
[Serializable]
public class ConcurrentHashSet<T> : ICollection<T>
public class ConcurrentHashSet<T> : ICollection<T>, ISet<T>
{
private readonly HashSet<T> _innerSet = new();
private readonly ReaderWriterLockSlim _instanceLocker = new(LockRecursionPolicy.NoRecursion);
@@ -274,4 +274,180 @@ public class ConcurrentHashSet<T> : ICollection<T>
return clone;
}
public void ExceptWith(IEnumerable<T> other)
{
try
{
_instanceLocker.EnterWriteLock();
_innerSet.ExceptWith(other);
}
finally
{
if (_instanceLocker.IsWriteLockHeld)
{
_instanceLocker.ExitWriteLock();
}
}
}
public void IntersectWith(IEnumerable<T> other)
{
try
{
_instanceLocker.EnterWriteLock();
_innerSet.IntersectWith(other);
}
finally
{
if (_instanceLocker.IsWriteLockHeld)
{
_instanceLocker.ExitWriteLock();
}
}
}
public bool IsProperSubsetOf(IEnumerable<T> other)
{
try
{
_instanceLocker.EnterReadLock();
return _innerSet.IsProperSubsetOf(other);
}
finally
{
if (_instanceLocker.IsReadLockHeld)
{
_instanceLocker.ExitReadLock();
}
}
}
public bool IsProperSupersetOf(IEnumerable<T> other)
{
try
{
_instanceLocker.EnterReadLock();
return _innerSet.IsProperSupersetOf(other);
}
finally
{
if (_instanceLocker.IsReadLockHeld)
{
_instanceLocker.ExitReadLock();
}
}
}
public bool IsSubsetOf(IEnumerable<T> other)
{
try
{
_instanceLocker.EnterReadLock();
return _innerSet.IsSubsetOf(other);
}
finally
{
if (_instanceLocker.IsReadLockHeld)
{
_instanceLocker.ExitReadLock();
}
}
}
public bool IsSupersetOf(IEnumerable<T> other)
{
try
{
_instanceLocker.EnterReadLock();
return _innerSet.IsSupersetOf(other);
}
finally
{
if (_instanceLocker.IsReadLockHeld)
{
_instanceLocker.ExitReadLock();
}
}
}
public bool Overlaps(IEnumerable<T> other)
{
try
{
_instanceLocker.EnterReadLock();
return _innerSet.Overlaps(other);
}
finally
{
if (_instanceLocker.IsReadLockHeld)
{
_instanceLocker.ExitReadLock();
}
}
}
public bool SetEquals(IEnumerable<T> other)
{
try
{
_instanceLocker.EnterReadLock();
return _innerSet.SetEquals(other);
}
finally
{
if (_instanceLocker.IsReadLockHeld)
{
_instanceLocker.ExitReadLock();
}
}
}
public void SymmetricExceptWith(IEnumerable<T> other)
{
try
{
_instanceLocker.EnterWriteLock();
_innerSet.IntersectWith(other);
}
finally
{
if (_instanceLocker.IsWriteLockHeld)
{
_instanceLocker.ExitWriteLock();
}
}
}
public void UnionWith(IEnumerable<T> other)
{
try
{
_instanceLocker.EnterWriteLock();
_innerSet.UnionWith(other);
}
finally
{
if (_instanceLocker.IsWriteLockHeld)
{
_instanceLocker.ExitWriteLock();
}
}
}
bool ISet<T>.Add(T item)
{
try
{
_instanceLocker.EnterWriteLock();
return _innerSet.Add(item);
}
finally
{
if (_instanceLocker.IsWriteLockHeld)
{
_instanceLocker.ExitWriteLock();
}
}
}
}
+2 -2
View File
@@ -11,7 +11,7 @@ namespace Umbraco.Cms.Core.Composing;
/// <summary>
/// Handles the composers.
/// </summary>
internal class ComposerGraph
internal sealed class ComposerGraph
{
private readonly IUmbracoBuilder _builder;
private readonly IEnumerable<Type> _composerTypes;
@@ -415,7 +415,7 @@ internal class ComposerGraph
}
}
private class EnableInfo
private sealed class EnableInfo
{
public bool Enabled { get; set; }
@@ -11,7 +11,7 @@ namespace Umbraco.Cms.Core.Composing;
/// borrowed and modified from here
/// https://github.com/dotnet/aspnetcore-tooling/blob/master/src/Razor/src/Microsoft.NET.Sdk.Razor/FindAssembliesWithReferencesTo.cs
/// </remarkes>
internal class FindAssembliesWithReferencesTo
internal sealed class FindAssembliesWithReferencesTo
{
private readonly bool _includeTargets;
private readonly ILogger<FindAssembliesWithReferencesTo> _logger;
+1 -1
View File
@@ -408,7 +408,7 @@ public sealed class TypeLoader
/// </summary>
/// <seealso cref="System.Exception" />
[Serializable]
internal class CachedTypeNotFoundInFileException : Exception
internal sealed class CachedTypeNotFoundInFileException : Exception
{
/// <summary>
/// Initializes a new instance of the <see cref="CachedTypeNotFoundInFileException" /> class.
@@ -2,7 +2,7 @@ using System.Reflection;
namespace Umbraco.Cms.Core.Configuration;
internal class EntryAssemblyMetadata : IEntryAssemblyMetadata
internal sealed class EntryAssemblyMetadata : IEntryAssemblyMetadata
{
public EntryAssemblyMetadata()
{
@@ -37,7 +37,7 @@ public class ContentErrorPage : ValidatableEntryBase
[Required]
public string Culture { get; set; } = null!;
internal override bool IsValid() =>
internal sealed override bool IsValid() =>
base.IsValid() &&
(HasContentId ^ HasContentKey);
}
@@ -0,0 +1,27 @@
// Copyright (c) Umbraco.
// See LICENSE for more details.
using System.ComponentModel;
namespace Umbraco.Cms.Core.Configuration.Models;
/// <summary>
/// Typed configuration options for long-running operations cleanup settings.
/// </summary>
public class LongRunningOperationsCleanupSettings
{
private const string StaticPeriod = "00:02:00";
private const string StaticMaxAge = "01:00:00";
/// <summary>
/// Gets or sets a value for the period in which long-running operations are cleaned up.
/// </summary>
[DefaultValue(StaticPeriod)]
public TimeSpan Period { get; set; } = TimeSpan.Parse(StaticPeriod);
/// <summary>
/// Gets or sets the maximum time a long-running operation entry can exist, without being updated, before it is considered for cleanup.
/// </summary>
[DefaultValue(StaticMaxAge)]
public TimeSpan MaxEntryAge { get; set; } = TimeSpan.Parse(StaticMaxAge);
}
@@ -0,0 +1,33 @@
// Copyright (c) Umbraco.
// See LICENSE for more details.
using System.ComponentModel;
namespace Umbraco.Cms.Core.Configuration.Models;
/// <summary>
/// Typed configuration options for long-running operations settings.
/// </summary>
[UmbracoOptions(Constants.Configuration.ConfigLongRunningOperations)]
public class LongRunningOperationsSettings
{
private const string StaticExpirationTime = "00:05:00";
private const string StaticTimeBetweenStatusChecks = "00:00:10";
/// <summary>
/// Gets or sets the cleanup settings for long-running operations.
/// </summary>
public LongRunningOperationsCleanupSettings Cleanup { get; set; } = new();
/// <summary>
/// Gets or sets the time after which a long-running operation is considered expired/stale, if not updated.
/// </summary>
[DefaultValue(StaticExpirationTime)]
public TimeSpan ExpirationTime { get; set; } = TimeSpan.Parse(StaticExpirationTime);
/// <summary>
/// Gets or sets the time between status checks for long-running operations.
/// </summary>
[DefaultValue(StaticTimeBetweenStatusChecks)]
public TimeSpan TimeBetweenStatusChecks { get; set; } = TimeSpan.Parse(StaticTimeBetweenStatusChecks);
}
@@ -21,6 +21,7 @@ public class WebRoutingSettings
internal const bool StaticDisableFindContentByIdentifierPath = false;
internal const bool StaticDisableRedirectUrlTracking = false;
internal const string StaticUrlProviderMode = "Auto";
internal const bool StaticUseStrictDomainMatching = false;
/// <summary>
/// Gets or sets a value indicating whether to check if any routed endpoints match a front-end request before
@@ -60,8 +61,12 @@ public class WebRoutingSettings
[DefaultValue(StaticValidateAlternativeTemplates)]
public bool ValidateAlternativeTemplates { get; set; } = StaticValidateAlternativeTemplates;
/// <summary>
/// Gets or sets a value indicating whether the content finder by a path of the content key (<see cref="Routing.ContentFinderByKeyPath" />) is disabled.
/// </summary>
[DefaultValue(StaticDisableFindContentByIdentifierPath)]
public bool DisableFindContentByIdentifierPath { get; set; } = StaticDisableFindContentByIdentifierPath;
/// <summary>
/// Gets or sets a value indicating whether redirect URL tracking is disabled.
/// </summary>
@@ -78,4 +83,15 @@ public class WebRoutingSettings
/// Gets or sets a value for the Umbraco application URL.
/// </summary>
public string UmbracoApplicationUrl { get; set; } = null!;
/// <summary>
/// Gets or sets a value indicating whether strict domain matching is used when finding content to match the request.
/// </summary>
/// <remarks>
/// <para>This setting is used within Umbraco's routing process based on content finders, specifically <see cref="Routing.ContentFinderByUrlNew" />.</para>
/// <para>If set to the default value of <see langword="false"/>, requests that don't match a configured domain will be routed to the first root node.</para>
/// <para>If set to <see langword="true"/>, requests that don't match a configured domain will not be routed.</para>
/// </remarks>
[DefaultValue(StaticUseStrictDomainMatching)]
public bool UseStrictDomainMatching { get; set; } = StaticUseStrictDomainMatching;
}
@@ -40,6 +40,7 @@ public static partial class Constants
public const string ConfigExamine = ConfigPrefix + "Examine";
public const string ConfigIndexing = ConfigPrefix + "Indexing";
public const string ConfigLogging = ConfigPrefix + "Logging";
public const string ConfigLongRunningOperations = ConfigPrefix + "LongRunningOperations";
public const string ConfigMemberPassword = ConfigPrefix + "Security:MemberPassword";
public const string ConfigModelsBuilder = ConfigPrefix + "ModelsBuilder";
public const string ConfigModelsMode = ConfigModelsBuilder + ":ModelsMode";
@@ -7,8 +7,6 @@ namespace Umbraco.Cms.Core.DeliveryApi;
public sealed class ApiContentBuilder : ApiContentBuilderBase<IApiContent>, IApiContentBuilder
{
private readonly IVariationContextAccessor _variationContextAccessor;
[Obsolete("Please use the constructor that takes an IVariationContextAccessor instead. Scheduled for removal in V17.")]
public ApiContentBuilder(
IApiContentNameProvider apiContentNameProvider,
@@ -27,9 +25,10 @@ public sealed class ApiContentBuilder : ApiContentBuilderBase<IApiContent>, IApi
IApiContentRouteBuilder apiContentRouteBuilder,
IOutputExpansionStrategyAccessor outputExpansionStrategyAccessor,
IVariationContextAccessor variationContextAccessor)
: base(apiContentNameProvider, apiContentRouteBuilder, outputExpansionStrategyAccessor)
=> _variationContextAccessor = variationContextAccessor;
: base(apiContentNameProvider, apiContentRouteBuilder, outputExpansionStrategyAccessor, variationContextAccessor)
{
}
protected override IApiContent Create(IPublishedContent content, string name, IApiContentRoute route, IDictionary<string, object?> properties)
=> new ApiContent(content.Key, name, content.ContentType.Alias, content.CreateDate, content.CultureDate(_variationContextAccessor), route, properties);
=> new ApiContent(content.Key, name, content.ContentType.Alias, content.CreateDate, content.CultureDate(VariationContextAccessor), route, properties);
}
@@ -1,5 +1,8 @@
using Umbraco.Cms.Core.Models.DeliveryApi;
using Microsoft.Extensions.DependencyInjection;
using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Core.Models.DeliveryApi;
using Umbraco.Cms.Core.Models.PublishedContent;
using Umbraco.Extensions;
namespace Umbraco.Cms.Core.DeliveryApi;
@@ -7,26 +10,63 @@ public abstract class ApiContentBuilderBase<T>
where T : IApiContent
{
private readonly IApiContentNameProvider _apiContentNameProvider;
private readonly IApiContentRouteBuilder _apiContentRouteBuilder;
private readonly IOutputExpansionStrategyAccessor _outputExpansionStrategyAccessor;
protected ApiContentBuilderBase(IApiContentNameProvider apiContentNameProvider, IApiContentRouteBuilder apiContentRouteBuilder, IOutputExpansionStrategyAccessor outputExpansionStrategyAccessor)
[Obsolete("Please use the constructor that takes all parameters. Scheduled for removal in Umbraco 17.")]
protected ApiContentBuilderBase(
IApiContentNameProvider apiContentNameProvider,
IApiContentRouteBuilder apiContentRouteBuilder,
IOutputExpansionStrategyAccessor outputExpansionStrategyAccessor)
: this(
apiContentNameProvider,
apiContentRouteBuilder,
outputExpansionStrategyAccessor,
StaticServiceProvider.Instance.GetRequiredService<IVariationContextAccessor>())
{
}
protected ApiContentBuilderBase(
IApiContentNameProvider apiContentNameProvider,
IApiContentRouteBuilder apiContentRouteBuilder,
IOutputExpansionStrategyAccessor outputExpansionStrategyAccessor,
IVariationContextAccessor variationContextAccessor)
{
_apiContentNameProvider = apiContentNameProvider;
_apiContentRouteBuilder = apiContentRouteBuilder;
ApiContentRouteBuilder = apiContentRouteBuilder;
_outputExpansionStrategyAccessor = outputExpansionStrategyAccessor;
VariationContextAccessor = variationContextAccessor;
}
protected IApiContentRouteBuilder ApiContentRouteBuilder { get; }
protected IVariationContextAccessor VariationContextAccessor { get; }
protected abstract T Create(IPublishedContent content, string name, IApiContentRoute route, IDictionary<string, object?> properties);
public virtual T? Build(IPublishedContent content)
{
IApiContentRoute? route = _apiContentRouteBuilder.Build(content);
IApiContentRoute? route = ApiContentRouteBuilder.Build(content);
if (route is null)
{
return default;
}
// If a segment is requested and no segmented properties have any values, we consider the segment as not created or non-existing and return null.
// This aligns the behaviour of the API when it comes to "Accept-Segment" and "Accept-Language" requests, so 404 is returned for both when
// the segment or language is not created or does not exist.
// It also aligns with what we show in the backoffice for whether a segment is "Published" or "Not created".
// Requested languages that aren't created or don't exist will already have exited early in the route builder.
var segment = VariationContextAccessor.VariationContext?.Segment;
if (segment.IsNullOrWhiteSpace() is false
&& content.ContentType.VariesBySegment()
&& content
.Properties
.Where(p => p.PropertyType.VariesBySegment())
.All(p => p.HasValue(VariationContextAccessor.VariationContext?.Culture, segment) is false))
{
return default;
}
IDictionary<string, object?> properties =
_outputExpansionStrategyAccessor.TryGetValue(out IOutputExpansionStrategy? outputExpansionStrategy)
? outputExpansionStrategy.MapContentProperties(content)
@@ -1,4 +1,4 @@
using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Core.Models.DeliveryApi;
using Umbraco.Cms.Core.Models.PublishedContent;
using Umbraco.Extensions;
@@ -7,9 +7,6 @@ namespace Umbraco.Cms.Core.DeliveryApi;
public class ApiContentResponseBuilder : ApiContentBuilderBase<IApiContentResponse>, IApiContentResponseBuilder
{
private readonly IApiContentRouteBuilder _apiContentRouteBuilder;
private readonly IVariationContextAccessor _variationContextAccessor;
[Obsolete("Please use the constructor that takes an IVariationContextAccessor instead. Scheduled for removal in V17.")]
public ApiContentResponseBuilder(
IApiContentNameProvider apiContentNameProvider,
@@ -28,16 +25,14 @@ public class ApiContentResponseBuilder : ApiContentBuilderBase<IApiContentRespon
IApiContentRouteBuilder apiContentRouteBuilder,
IOutputExpansionStrategyAccessor outputExpansionStrategyAccessor,
IVariationContextAccessor variationContextAccessor)
: base(apiContentNameProvider, apiContentRouteBuilder, outputExpansionStrategyAccessor)
: base(apiContentNameProvider, apiContentRouteBuilder, outputExpansionStrategyAccessor, variationContextAccessor)
{
_apiContentRouteBuilder = apiContentRouteBuilder;
_variationContextAccessor = variationContextAccessor;
}
protected override IApiContentResponse Create(IPublishedContent content, string name, IApiContentRoute route, IDictionary<string, object?> properties)
{
IDictionary<string, IApiContentRoute> cultures = GetCultures(content);
return new ApiContentResponse(content.Key, name, content.ContentType.Alias, content.CreateDate, content.CultureDate(_variationContextAccessor), route, properties, cultures);
return new ApiContentResponse(content.Key, name, content.ContentType.Alias, content.CreateDate, content.CultureDate(VariationContextAccessor), route, properties, cultures);
}
protected virtual IDictionary<string, IApiContentRoute> GetCultures(IPublishedContent content)
@@ -52,7 +47,7 @@ public class ApiContentResponseBuilder : ApiContentBuilderBase<IApiContentRespon
continue;
}
IApiContentRoute? cultureRoute = _apiContentRouteBuilder.Build(content, publishedCultureInfo.Culture);
IApiContentRoute? cultureRoute = ApiContentRouteBuilder.Build(content, publishedCultureInfo.Culture);
if (cultureRoute == null)
{
// content is un-routable in this culture
@@ -69,6 +69,7 @@ public static partial class UmbracoBuilderExtensions
.AddUmbracoOptions<ImagingSettings>()
.AddUmbracoOptions<IndexingSettings>()
.AddUmbracoOptions<LoggingSettings>()
.AddUmbracoOptions<LongRunningOperationsSettings>()
.AddUmbracoOptions<MemberPasswordConfigurationSettings>()
.AddUmbracoOptions<NuCacheSettings>()
.AddUmbracoOptions<RequestHandlerSettings>()
@@ -34,6 +34,7 @@ using Umbraco.Cms.Core.Scoping;
using Umbraco.Cms.Core.Security;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Services.ContentTypeEditing;
using Umbraco.Cms.Core.HostedServices;
using Umbraco.Cms.Core.Preview;
using Umbraco.Cms.Core.PublishedCache;
using Umbraco.Cms.Core.PublishedCache.Internal;
@@ -341,6 +342,7 @@ namespace Umbraco.Cms.Core.DependencyInjection
Services.AddUnique<ILocalizedTextService>(factory => new LocalizedTextService(
factory.GetRequiredService<Lazy<LocalizedTextServiceFileSources>>(),
factory.GetRequiredService<ILogger<LocalizedTextService>>()));
Services.AddUnique<ILongRunningOperationService, LongRunningOperationService>();
Services.AddUnique<IEntityXmlSerializer, EntityXmlSerializer>();
+1 -1
View File
@@ -1,6 +1,6 @@
namespace Umbraco.Cms.Core.Diagnostics;
internal class NoopMarchal : IMarchal
internal sealed class NoopMarchal : IMarchal
{
public IntPtr GetExceptionPointers() => IntPtr.Zero;
}
@@ -17,7 +17,7 @@ namespace Umbraco.Cms.Core.Dictionary;
/// fast
/// (even though there is caching involved, if there's lots of dictionary items the caching is not great)
/// </remarks>
internal class DefaultCultureDictionary : ICultureDictionary
internal sealed class DefaultCultureDictionary : ICultureDictionary
{
private readonly ILocalizationService _localizationService;
private readonly IAppCache _requestCache;
+1 -1
View File
@@ -5,7 +5,7 @@ namespace Umbraco.Cms.Core;
/// <summary>
/// Currently just used to get the machine name for use with file names
/// </summary>
internal class EnvironmentHelper
internal static class EnvironmentHelper
{
/// <summary>
/// Returns the machine name that is safe to use in file paths.
@@ -102,7 +102,7 @@ public partial class EventAggregator : IEventAggregator
}
}
private void PublishCore<TNotification>(IEnumerable<Action<IEnumerable<TNotification>>> allHandlers, IEnumerable<TNotification> notifications)
private static void PublishCore<TNotification>(IEnumerable<Action<IEnumerable<TNotification>>> allHandlers, IEnumerable<TNotification> notifications)
{
foreach (Action<IEnumerable<TNotification>> handler in allHandlers)
{
@@ -110,7 +110,7 @@ public partial class EventAggregator : IEventAggregator
}
}
private async Task PublishCoreAsync<TNotification>(IEnumerable<Func<IEnumerable<TNotification>, CancellationToken, Task>> allHandlers, IEnumerable<TNotification> notifications, CancellationToken cancellationToken)
private static async Task PublishCoreAsync<TNotification>(IEnumerable<Func<IEnumerable<TNotification>, CancellationToken, Task>> allHandlers, IEnumerable<TNotification> notifications, CancellationToken cancellationToken)
{
foreach (Func<IEnumerable<TNotification>, CancellationToken, Task> handler in allHandlers)
{
@@ -192,7 +192,7 @@ internal abstract class NotificationAsyncHandlerWrapper
where TNotificationHandler : INotificationHandler;
}
internal class NotificationAsyncHandlerWrapperImpl<TNotificationType> : NotificationAsyncHandlerWrapper
internal sealed class NotificationAsyncHandlerWrapperImpl<TNotificationType> : NotificationAsyncHandlerWrapper
where TNotificationType : INotification
{
/// <remarks>
@@ -260,7 +260,7 @@ internal class NotificationAsyncHandlerWrapperImpl<TNotificationType> : Notifica
}
}
internal class NotificationHandlerWrapperImpl<TNotificationType> : NotificationHandlerWrapper
internal sealed class NotificationHandlerWrapperImpl<TNotificationType> : NotificationHandlerWrapper
where TNotificationType : INotification
{
/// <remarks>
@@ -169,7 +169,7 @@ public class EventNameExtractor
return words[0].EndsWith("ing") == false;
}
private class EventInfoArgs
private sealed class EventInfoArgs
{
public EventInfoArgs(EventInfo eventInfo, Type[] genericArgs)
{
@@ -7,7 +7,7 @@ namespace Umbraco.Cms.Core.Events;
/// This means that events will be raised during the scope transaction,
/// whatever happens, and the transaction could roll back in the end.
/// </remarks>
internal class PassThroughEventDispatcher : IEventDispatcher
internal sealed class PassThroughEventDispatcher : IEventDispatcher
{
public bool DispatchCancelable(EventHandler? eventHandler, object sender, CancellableEventArgs args, string? eventName = null)
{
@@ -414,7 +414,7 @@ public abstract class QueuingEventDispatcherBase : IEventDispatcher
}
}
private class EventDefinitionInfos
private sealed class EventDefinitionInfos
{
public IEventDefinition? EventDefinition { get; set; }
@@ -116,7 +116,7 @@ public class ScopedNotificationPublisher<TNotificationHandler> : IScopedNotifica
protected virtual void PublishScopedNotifications(IList<INotification> notifications)
=> _eventAggregator.Publish<INotification, TNotificationHandler>(notifications);
private class Suppressor : IDisposable
private sealed class Suppressor : IDisposable
{
private readonly ScopedNotificationPublisher<TNotificationHandler> _scopedNotificationPublisher;
private bool _disposedValue;
@@ -129,7 +129,7 @@ public class ScopedNotificationPublisher<TNotificationHandler> : IScopedNotifica
public void Dispose() => Dispose(true);
protected virtual void Dispose(bool disposing)
private void Dispose(bool disposing)
{
if (!_disposedValue)
{
@@ -5,7 +5,6 @@ using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Runtime.InteropServices;
using System.Security.Cryptography;
using System.Text;
using System.Text.RegularExpressions;
@@ -96,7 +96,7 @@ public class WarnDocumentTypeElementSwitchNotificationHandler :
}
}
private class DocumentTypeElementSwitchInformation
private sealed class DocumentTypeElementSwitchInformation
{
public bool WasElement { get; set; }
}
@@ -1,6 +1,6 @@
namespace Umbraco.Cms.Core.Hosting;
internal class NoopApplicationShutdownRegistry : IApplicationShutdownRegistry
internal sealed class NoopApplicationShutdownRegistry : IApplicationShutdownRegistry
{
public void RegisterObject(IRegisteredObject registeredObject)
{
+3 -3
View File
@@ -2,7 +2,7 @@ using System.Text.RegularExpressions;
namespace Umbraco.Cms.Core.IO;
internal class ShadowFileSystem : IFileSystem
internal sealed class ShadowFileSystem : IFileSystem
{
private readonly IFileSystem _sfs;
@@ -392,7 +392,7 @@ internal class ShadowFileSystem : IFileSystem
}
// copied from System.Web.Util.Wildcard internal
internal class WildcardExpression
internal sealed class WildcardExpression
{
private static readonly Regex MetaRegex = new("[\\+\\{\\\\\\[\\|\\(\\)\\.\\^\\$]");
private static readonly Regex QuestRegex = new("\\?");
@@ -454,7 +454,7 @@ internal class ShadowFileSystem : IFileSystem
}
}
private class ShadowNode
private sealed class ShadowNode
{
public ShadowNode(bool isDelete, bool isdir)
{
+1 -1
View File
@@ -1,7 +1,7 @@
namespace Umbraco.Cms.Core.IO;
// shadow filesystems is definitively ... too convoluted
internal class ShadowFileSystems : ICompletable
internal sealed class ShadowFileSystems : ICompletable
{
private readonly FileSystems _fileSystems;
private bool _completed;
+1 -1
View File
@@ -5,7 +5,7 @@ using Umbraco.Extensions;
namespace Umbraco.Cms.Core.IO;
internal class ShadowWrapper : IFileSystem, IFileProviderFactory
internal sealed class ShadowWrapper : IFileSystem, IFileProviderFactory
{
private const string ShadowFsPath = "ShadowFs";
+1 -1
View File
@@ -39,7 +39,7 @@ public class LogProfiler : IProfiler
public bool IsEnabled => _logger.IsEnabled(Microsoft.Extensions.Logging.LogLevel.Debug);
// a lightweight disposable timer
private class LightDisposableTimer : DisposableObjectSlim
private sealed class LightDisposableTimer : DisposableObjectSlim
{
private readonly Action<long> _callback;
private readonly Stopwatch _stopwatch = Stopwatch.StartNew();
+1 -1
View File
@@ -17,7 +17,7 @@ public class NoopProfiler : IProfiler
/// <inheritdoc/>
public bool IsEnabled => false;
private class VoidDisposable : DisposableObjectSlim
private sealed class VoidDisposable : DisposableObjectSlim
{
protected override void DisposeResources()
{
@@ -2,7 +2,7 @@ using Umbraco.Cms.Core.Models.Email;
namespace Umbraco.Cms.Core.Mail;
internal class NotImplementedEmailSender : IEmailSender
internal sealed class NotImplementedEmailSender : IEmailSender
{
public Task SendAsync(EmailMessage message, string emailType)
=> throw new NotImplementedException(
@@ -3,7 +3,7 @@ namespace Umbraco.Cms.Core.Mail;
/// <summary>
/// An <see cref="ISmsSender" /> that throws <see cref="NotImplementedException" />
/// </summary>
internal class NotImplementedSmsSender : ISmsSender
internal sealed class NotImplementedSmsSender : ISmsSender
{
public Task SendSmsAsync(string number, string message)
=> throw new NotImplementedException(
+21 -1
View File
@@ -8,19 +8,39 @@ public class CacheSettings
{
internal const int StaticDocumentBreadthFirstSeedCount = 100;
internal const int StaticMediaBreadthFirstSeedCount = 100;
internal const int StaticDocumentSeedBatchSize = 100;
internal const int StaticMediaSeedBatchSize = 100;
/// <summary>
/// Gets or sets a value for the collection of content type ids to always have in the cache.
/// Gets or sets a value for the collection of content type ids to always have in the cache.
/// </summary>
public List<Guid> ContentTypeKeys { get; set; } =
new();
/// <summary>
/// Gets or sets a value for the document breadth first seed count.
/// </summary>
[DefaultValue(StaticDocumentBreadthFirstSeedCount)]
public int DocumentBreadthFirstSeedCount { get; set; } = StaticDocumentBreadthFirstSeedCount;
/// <summary>
/// Gets or sets a value for the media breadth first seed count.
/// </summary>
[DefaultValue(StaticMediaBreadthFirstSeedCount)]
public int MediaBreadthFirstSeedCount { get; set; } = StaticDocumentBreadthFirstSeedCount;
/// <summary>
/// Gets or sets a value for the document seed batch size.
/// </summary>
[DefaultValue(StaticDocumentSeedBatchSize)]
public int DocumentSeedBatchSize { get; set; } = StaticDocumentSeedBatchSize;
/// <summary>
/// Gets or sets a value for the media seed batch size.
/// </summary>
[DefaultValue(StaticMediaSeedBatchSize)]
public int MediaSeedBatchSize { get; set; } = StaticMediaSeedBatchSize;
public CacheEntry Entry { get; set; } = new CacheEntry();
public class CacheEntry
@@ -0,0 +1,14 @@
namespace Umbraco.Cms.Core.Models.ContentPublishing;
internal sealed class ContentPublishingBranchInternalResult
{
public Guid? ContentKey { get; init; }
public IContent? Content { get; init; }
public IEnumerable<ContentPublishingBranchItemResult> SucceededItems { get; set; } = [];
public IEnumerable<ContentPublishingBranchItemResult> FailedItems { get; set; } = [];
public Guid? AcceptedTaskId { get; init; }
}
+6 -1
View File
@@ -17,6 +17,7 @@ public class DataType : TreeEntityBase, IDataType
private IDictionary<string, object> _configurationData;
private ValueStorageType _databaseType;
private IDataEditor? _editor;
private string? _editorUiAlias;
private bool _hasConfigurationObject;
/// <summary>
@@ -60,7 +61,11 @@ public class DataType : TreeEntityBase, IDataType
/// <inheritdoc />
[DataMember]
public string? EditorUiAlias { get; set; }
public string? EditorUiAlias
{
get => _editorUiAlias;
set => SetPropertyValueAndDetectChanges(value, ref _editorUiAlias, nameof(EditorUiAlias));
}
/// <inheritdoc />
[DataMember]
@@ -0,0 +1,17 @@
namespace Umbraco.Cms.Core.Models;
/// <summary>
/// Represents the result of a database cache rebuild operation.
/// </summary>
public enum DatabaseCacheRebuildResult
{
/// <summary>
/// The cache rebuild operation was either successful or enqueued successfully.
/// </summary>
Success,
/// <summary>
/// A cache rebuild operation is already in progress.
/// </summary>
AlreadyRunning,
}
@@ -1,6 +1,6 @@
namespace Umbraco.Cms.Core.Models;
internal class DefaultPayloadModel
internal sealed class DefaultPayloadModel
{
public Guid Id { get; set; }
}
@@ -0,0 +1,22 @@
namespace Umbraco.Cms.Core.Models;
/// <summary>
/// Represents a long-running operation.
/// </summary>
public class LongRunningOperation
{
/// <summary>
/// Gets the unique identifier for the long-running operation.
/// </summary>
public required Guid Id { get; init; }
/// <summary>
/// Gets or sets the type of the long-running operation.
/// </summary>
public required string Type { get; set; }
/// <summary>
/// Gets or sets the status of the long-running operation.
/// </summary>
public required LongRunningOperationStatus Status { get; set; }
}
@@ -0,0 +1,13 @@
namespace Umbraco.Cms.Core.Models;
/// <summary>
/// Represents a long-running operation.
/// </summary>
/// <typeparam name="TResult">The type of the result of the long-running operation.</typeparam>
public class LongRunningOperation<TResult> : LongRunningOperation
{
/// <summary>
/// Gets or sets the result of the long-running operation.
/// </summary>
public TResult? Result { get; set; }
}
@@ -0,0 +1,32 @@
namespace Umbraco.Cms.Core.Models;
/// <summary>
/// Represents the status of a long-running operation.
/// </summary>
public enum LongRunningOperationStatus
{
/// <summary>
/// The operation has finished successfully.
/// </summary>
Success,
/// <summary>
/// The operation has failed.
/// </summary>
Failed,
/// <summary>
/// The operation has been queued.
/// </summary>
Enqueued,
/// <summary>
/// The operation is currently running.
/// </summary>
Running,
/// <summary>
/// The operation wasn't updated within the expected time frame and is considered stale.
/// </summary>
Stale,
}
+1 -1
View File
@@ -426,7 +426,7 @@ public class User : EntityBase, IUser, IProfile
/// <summary>
/// Internal class used to wrap the user in a profile
/// </summary>
private class WrappedUserProfile : IProfile
private sealed class WrappedUserProfile : IProfile
{
private readonly IUser _user;
@@ -1,10 +1,11 @@
using System.Collections.Concurrent;
using Umbraco.Cms.Core.Collections;
namespace Umbraco.Cms.Core.Models.Navigation;
public sealed class NavigationNode
{
private HashSet<Guid> _children;
private ConcurrentHashSet<Guid> _children;
public Guid Key { get; private set; }
@@ -21,7 +22,7 @@ public sealed class NavigationNode
Key = key;
ContentTypeKey = contentTypeKey;
SortOrder = sortOrder;
_children = new HashSet<Guid>();
_children = new ConcurrentHashSet<Guid>();
}
public void UpdateSortOrder(int newSortOrder) => SortOrder = newSortOrder;
@@ -373,7 +373,7 @@ public class ModelType : Type
}
/// <inheritdoc />
internal class ModelTypeArrayType : Type
internal sealed class ModelTypeArrayType : Type
{
private readonly Type _elementType;
@@ -3,7 +3,7 @@ using System.Globalization;
namespace Umbraco.Cms.Core.Models.PublishedContent;
internal class PublishedContentTypeConverter : TypeConverter
internal sealed class PublishedContentTypeConverter : TypeConverter
{
private static readonly Type[] ConvertingTypes = { typeof(int) };
@@ -151,7 +151,7 @@ public class PublishedModelFactory : IPublishedModelFactory
public Type MapModelType(Type type)
=> ModelType.Map(type, _modelTypeMap);
private class ModelInfo
private sealed class ModelInfo
{
public Type? ParameterType { get; set; }

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