Compare commits

...
Author SHA1 Message Date
NathanWoulfe dcffbc65f4 provide document and documentType IDs to ISegmentService 2025-07-23 12:40:24 +10:00
Nhu DinhandGitHub 6d240b6a07 V16 QA Added acceptance tests for creating a document using blueprint (#19708)
* Added tests for creating content using document blueprint

* Updated tests due to api helper for document blueprint changes

* Bumped version

* Make all Blueprint tests runs in the pipeline

* Reverted npm command
2025-07-21 10:33:30 +07:00
Nhu DinhandGitHub d31b88b94a V16 Added acceptance tests for the regression issue #19529 (#19713)
* Added tests for regression issue that cannot create a variant document blueprint

* Make tests run in the pipeline

* Reverted npm command
2025-07-21 03:23:44 +00:00
Nhu DinhandGitHub 0442ddc739 V16 Added acceptance tests for the regression issue #16985 (#19712)
* Updated tests for adding a thumbnail to a block grid

* Added tests for adding a block thumbnail

* Make tests run in the pipeline

* Reverted npm command
2025-07-21 03:21:21 +00:00
3b04d54ea3 Add flag to exclude trashed media (#19746)
Co-authored-by: Lucas Bach Bisgaard <lucas.bisgaard@kraftvaerk.com>
2025-07-18 14:59:06 +02:00
Kenn JacobsenandGitHub 71df46cc06 Include /.well-known/jwks in the paths covered by OpenIddict (#19749) 2025-07-18 13:48:22 +02:00
5660c6c369 Forward merge #19720 to V16 (#19735)
* Add support for programmatic creation of property types providing the data type key (#19720)

* Add support for programmatic creation of property types providing the data type key.

* Add integration tests

---------

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

* Don't use Lazy

---------

Co-authored-by: Andy Butland <abutland73@gmail.com>
2025-07-18 10:47:34 +02:00
698d566b76 Populate name for content and media on URL picker if title is left empty (#19677)
* Populate name for content and media on URL picker if title is left empty.

* Display URL for manually entered URLs.

* Updates from code review.

* Reverted `elementName` constant

* Sorted imports

* Small code tidy-ups

* Added logic to render the `url` as the `name` fallback

In this case, the `detail` is left empty, giving prominence to the `url` value.

* Refactored the get name/url methods

for code consistency.

* Updated `#requestRemoveItem()` to use the item's resolved name

with a fallback to "item".

Also localized the "Remove" button label

---------

Co-authored-by: leekelleher <leekelleher@gmail.com>
2025-07-15 13:31:33 +01:00
5d968afb7e Fix failing integration tests in SQL Server (#19725)
Co-authored-by: Kenn Jacobsen <kja@umbraco.dk>
2025-07-15 09:01:04 +02:00
Jacob OvergaardandGitHub 63ff9e9e32 V16: Build Backoffice externals with Vite (#19716)
* chore: revamps openid package to organise files in a 'src' folder

* feat: adds all externals as npm workspaces with a vite build

* feat: copies the correct uui assets

* feat: copies the backoffice static assets

* feat: creates the correct module for openid

* feat: copies the correct monaco-editor assets

* feat: moves monaco-editor into its package

* feat: moves dependencies to relevant external modules

* feat: gets rid of rollup

* build: uses tiny-glob instead of glob (one less dependency)

* feat: copies all css assets to dist-cms/css first, minifies them, then copies everything over to StaticAssets

* build: removes old static assets from vite static build
2025-07-14 11:47:00 +01:00
Jacob OvergaardandGitHub d95b6b7530 V16: Property editor file upload does not validate file types (#19714)
* fix: forwards the file extensions to the inner dropzone

* fix: ensures non-mimetype extensions start with a dot (.)

* chore: adds more details on how to set file extensions

* feat: adds a bit of styling to code snippets in UFM

* fix: return if no file in stream

* fix: prevents potential race condition if src changes

* chore: minor code improvement
2025-07-14 09:04:58 +01:00
Andy Butland 4deb756e64 Adds variation by the header name Accept-Language to the delivery API output cache policy (#19709)
* Adds variation by the header name Accept-Language to the develivery API output cache policy

* Removed obsolete constructor (not necessary as the class is internal).

* Introduce contants for header names.
2025-07-11 16:08:18 +02:00
543b644ed1 Fix issue forceHideContentEditorInOverlay not available in RTE (#19698)
* Fix issue forceHideContentEditorInOverlay not available in RTE

* remove href link when enable hide content editor setting:

---------

Co-authored-by: Lan Nguyen Thuy <lnt@umbraco.dk>
2025-07-11 07:54:03 +02:00
e6c169e566 Adds background worker to check timeout state (#19702)
* feat: converts tokenResponse into an object state

* feat: adds worker that checks token lifetime

* feat: initialises token worker to check up on tokens

* revert

* chore: defines typings for shared workers

* chore: uses correct assets url for core package

* feat: sets correct values for token check expiration

* feat: adds labels to confirm modal

* feat: separates logic for session monitoring to own controller

* feat: adds a timeout modal to correctly inform the user

* feat: opens the timeout modal (and closes it again) if a timeout occurs

* feat: log out when user clicks log out button

* feat: adds localization

* feat: sets sensible defaults for the web worker to check

* feat: adds more languages

* chore: adds more comments

* chore: removes nodejs types

* Update src/Umbraco.Web.UI.Client/src/packages/core/auth/workers/token-check.worker.ts

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

* chore: removes nodejs types

* chore: resolves cyclic imports

* chore: removes circular dependencies from the 'modal' package

* chore: redefine SharedWorkerGlobalScope because of Github Actions CI

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-07-10 13:56:03 +00:00
Lee KelleherandGitHub 3c6f222c8b Fixes Block Catalogue Modal Filter (#19700)
* Mock data updates

The `icon` is not part the block-type data.

* Adds `description` to the mock doctype model

* Refactors block catalogue modal

to make the filter/search work with a block-type's name & description.

This removes the need to use the `<umb-block-type-card>` component,
all element-type data is requested upfront.

* Reverted dev/debug change

* Abstracted out the element-type items observation to its own method

* Updated CSS rule

thanks to a Copilot suggestion.
2025-07-10 09:33:56 +00:00
Andy Butland 50282ea2e9 Bumped version to 16.2.0-rc. 2025-07-10 11:08:54 +02:00
Andy Butland a91aefc185 Added comments for no-op partial view cache invalidator. 2025-07-10 07:33:07 +02:00
Lee KelleherandGitHub 1fe29d921b UFM: JavaScript-like Expressions (#19685)
* Adds `markedExtension` extension-type

* Relocates the Component and Filter extension-type interface code files

to under "extensions"

* Moves `UfmPlugin` type to its own referencable file

* Adds UFM support for JS expressions

making use of "@heximal/expressions" library.

* Modified regex pattern to match nested braces

* try/catch for invalid JS expressions

* Capitalizing the JS in `UmbUfmJsMarkedExtensionApi` class name

for consistency and improved readability.

* Abstracted out `ufmjs()` to its own Marked extension file

making it simpler to add unit-tests.

* Fixed up types in UFM context

added JSDocs for public methods

* Adds a generic Least Recently Used (LRU) cache implementation
2025-07-09 11:34:12 +02:00
Nhu DinhandGitHub 91e19c4faf V16 QA Reverted npx command (#19699)
* Reverted npx command

* Fixed npx command
2025-07-09 09:07:05 +00:00
Nhu DinhandGitHub 60580ff8bd V16 QA Added acceptance tests for granular document permission (#19546)
* Added tests for granular document permission

* Updated tests for Webhook

* Bumped version

* Make all tests for granular permission run in the pipeline

* Added issue link for the failing tests

* Remove .skip

* Removed unnecessary tests

* Updated assertion step for create and delete document for a specific document

* Updated tests for read permission

* Fixed comments
2025-07-09 13:30:47 +07:00
Nhu DinhandGitHub 17bc6dde28 V16 QA Removed unused API acceptance tests (#19679)
Removed unused API acceptance tests
2025-07-09 03:41:01 +00:00
Andy Butland 4e6f3f6d1d Merge branch 'main' of https://github.com/umbraco/Umbraco-CMS 2025-07-08 15:53:52 +02:00
Andy Butland 9bd3076abf Register no-op implementation of IMemberPartialViewCacheInvalidator in headless setups (#19666)
* Register no-op implementation of IMemberPartialViewCacheInvalidator in headless setups.

* Tidied usings.
# Conflicts:
#	src/Umbraco.Core/DependencyInjection/UmbracoBuilder.cs
2025-07-08 15:53:37 +02:00
05010fc707 UFM: Code tidy-up (#19687)
* Reverted `elementName` constant

* UFM base element: simplify context check

* Adds console warning if UFM filter does not exist

* Removed unnecessary casting/typing

* Apply suggestion from @Copilot

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

* Moved console warnings outside of loop

* Added "ufm-" prefix to eslint local rules

---------

Co-authored-by: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-07-08 14:21:47 +02:00
Andy ButlandandGitHub 118b26a8b9 Clear member cache by older user name when member user name is updated (16) (#19690)
* Pass notification state to cache refreshers.
Pass previous user name into member saved notification state and use when refreshing cache to clear the member by keys based on this.

* Fixed issue raised in code review.

* Fixed casing for state key.

* Added removed parameter to unit tests.

* Fix breaking change.
2025-07-08 11:28:11 +02:00
Lee KelleherandGitHub e00dcef4d6 UFM: Make Block Settings data available (#19686)
* Adds support to UFM for Block settings data

Fixes #17619

* Code/markup tidy-up

makes use of Lit's `when` directive
to simplify the conditional.
2025-07-08 10:12:58 +02:00
Lee KelleherandGitHub 1f84fe4701 UFM: Adds markedExtension extension-type (#19684)
* Adds `markedExtension` extension-type

* Relocates the Component and Filter extension-type interface code files

to under "extensions"

* Moves `UfmPlugin` type to its own referencable file
2025-07-08 10:10:22 +02:00
9baf04026e V16: Siblings endpoints (#19657)
* PoC implementation

* Move to controller base

* Implement solution that seems worse, but works better

* Don't require parent key in repository method

* Fix typos

* Add siblings for data type, media type and media

* Add endpoint for template

* Add DocumentType and DocumentBlueprint controllers

* Fix naming

* Fix case if siblings are under root

* Take item ordering into account

not all entities are ordered by sort order

* Add default implementation

* Fix parentkey

* Add tests

* Format optimizations for split view

* Add test covered requirement to description

* Cover positive case and make test case output more readable

* reduce allocations

* Clarify test

---------

Co-authored-by: Migaroez <geusens@gmail.com>
2025-07-07 12:53:42 +00:00
Andy ButlandandGitHub b5195ed8eb Fixes issues with creation of documents from blueprints that have populated file upload properties (#19655)
* Fixes issue where content created from blueprint would not persist file upload property values.

* Ensure a copy of a file upload is created when scaffolding content from a blueprint, like we do when copying content.

* Clarified comment.

* Removed unneeded usings.

* Fixed spelling.

* Handle create of blueprint from content to create a new uploaded file.
Handle delete of blueprint to delete uploaded files.
2025-07-07 14:15:17 +02:00
Andy ButlandandGitHub 4c87cc5fd9 Adds support for custom granular permissions when aggregating across user groups (#19660)
* Added abstraction for aggregation of granular permissions to support custom permissions.

* Refactor to move responsibility for aggregating granular permissions to the respective mappers.

* Added XML header comments for permission mappers.

* Tidied up/removed warnings in UserPresentationFactory interface and implementation.

* Optimized retrieval of documents in DocumentPermissionMapper.

* Fixed method header comment.

* Use entity service rather than content service to retrieve key and path.
2025-07-07 12:59:55 +02:00
Andy ButlandandGitHub 8b4849be05 Fix issue with preview in delivery API for MNTP property editor (#19668)
* Passes the preview flag to the cache retrieval when resolving the delivery API object for the MNTP property editor.

* Added unit test verifying fix and adjusted mocks for tests to acoomodate.

* Provided preview flag for Razor rendering.
2025-07-07 11:33:59 +02:00
PeterandGitHub 7b929a7c53 Title returns "Server Error" in non-debug mode (#19665)
* Add isDebug check for Title

* Error title has been added
2025-07-07 10:04:10 +02:00
Mads RasmussenandGitHub af52e68b6e Clean up workspace conditions folder structure (#19634)
* Refactor section conditions into subfolders

Split section condition logic into 'section-alias' and 'section-user-permission' subfolders, each with their own constants, manifests, and types. Updated imports and manifest aggregation to use the new structure for improved modularity and maintainability.

* use const

* fix build

* Refactor section alias condition to use constant

Replaces hardcoded 'Umb.Condition.SectionAlias' strings with the UMB_SECTION_ALIAS_CONDITION_ALIAS constant across all manifests and related files. This improves maintainability and consistency by centralizing the section alias condition reference.

* clean up workspace conditions
2025-07-07 07:34:01 +01:00
Mads RasmussenandGitHub 05aef6a91f Hide Document/Media Collection Workspace View until the item is created (#19644)
* only show collection items workspace view when document is created

* do not pass null for collection

* only show media collection view when media is created
2025-07-07 07:31:27 +01:00
Mads RasmussenandGitHub 8489d1a63e Docs: Tree example (#19624)
* add basic collection example

* add card view example

* update example readme

* Add workspace view example with collection

* wip tree example

* clean up

* Update README.md

* Update README.md
2025-07-07 07:13:16 +01:00
PeterandGitHub e0a00bfd04 Amend accessibility modifiers on file upload property editor components to support extension (#19643)
* Logic of getting media path moved to separate method.

* TemporaryFileUploadValidator marked as public

* Typo fix.
2025-07-04 14:22:50 +02:00
Jacob Overgaard 914bc409f6 Merge branch 'release/16.1' 2025-07-03 14:16:29 +02:00
Jacob OvergaardandGitHub aa90253d71 build(deps): bump @umbraco-ui from 1.14.1 to 1.14.2 (#19656) 2025-07-03 11:27:37 +02:00
Jacob OvergaardandGitHub c9bef96c32 V16.1: Never reject a token response (#19651)
* fix: never reject a token response

If a token response is rejected, then the pipeline will also fail because it does not understand that error. Let the API interceptors do their job instead and simply return the old, now-invalid token which will prompt the API interceptors to store the request states and retry them afterwards.

* chore: removes unused timeoutsignal

* chore: captures the stale token before potentially clearing it
2025-07-02 16:10:30 +01:00
Jacob Overgaard 9cb8dc73e5 Merge branch 'release/16.1' 2025-07-02 10:21:20 +02:00
Jacob OvergaardandGitHub d10ba420f4 V16.1: 400 errors are being wrongfully ignored (#19648)
fix: allows 400 errors to have a default notification unless silenced manually by `disableNotifications`
2025-07-02 08:16:03 +00:00
Jacob OvergaardandGitHub be3a1759de GitHub: Only deploy to Azure on direct pull requests (#19647)
* build(github): check that the "close" job only runs when the appropriate label is applied

it follows that the "build" job would only have built an environment when the label was applied

* build(github): check that the action is run directly on the repository and not from a fork

this alleviates the problem that the deploymentToken for Azure only exists within the repository
2025-07-02 09:47:53 +02:00
Laura NetoandGitHub dcd8b42522 Move cache instructions pruning to background job (#19598)
* Remove pruning logic from `CacheInstructionService.ProcessInstructions()`

* Add and register `CacheInstructionsPruningJob` background job

* Add unit tests

* Remove breaking change in ICacheInstructionService

* Adjust some obsoletion messages to mention v17

* Added missing scope

* Update tests

* Fix obsoletion messages version

* Update ProcessInstructions methods summary
2025-07-01 11:17:59 +02:00
3b4639de08 Collection rendering performance improvements Part 1: Improve Entity actions render performance (#19605)
* Add null checks for editPath and name in render method

The render method now checks for the presence of both editPath and _name before rendering the button, preventing potential errors when these values are missing.

* Refactor dropdown open state handling

Replaces the public 'open' property with a private field and getter/setter to better control dropdown state. Moves popover open/close logic into the setter, removes the 'updated' lifecycle method, and conditionally renders dropdown content based on the open state.

* add opened and closed events

* dispatch opened and closed events

* Render dropdown content only when open

Introduces an _isOpen state to control rendering of the dropdown content in UmbEntityActionsBundleElement. Dropdown content is now only rendered when the dropdown is open, improving performance and preventing unnecessary DOM updates.

* Update dropdown.element.ts

* create a cache elements

* Optimize entity actions observation with IntersectionObserver

Adds an IntersectionObserver to only observe entity actions when the element is in the viewport, improving performance. Refactors element creation to use constructors, updates event handling, and ensures cleanup in disconnectedCallback.

* only observe once

* Update entity-actions-bundle.element.ts

* Update dropdown.element.ts

* Update entity-actions-bundle.element.ts

* split dropdown component

* pass compact prop

* fix label

* Update entity-actions-dropdown.element.ts

* Update entity-actions-dropdown.element.ts

---------

Co-authored-by: Niels Lyngsø <nsl@umbraco.dk>
2025-07-01 08:37:57 +00:00
Jacob OvergaardandGitHub d5159ae4f7 V16: Upgrade Storybook from V8 to V9 (#19640)
* build(deps-dev): bump storybook from v8 to v9

* chore: run storybook v9 migrations

* chore: updates import paths for storybook-webcomponents-vite (migration)

* chore: migrates eslint for storybook config

* fix: updates old link to composed storybook so we reference the latest production uui

* chore: formats eslint config file

* chore: changes import path to build mdx stories

* chore: updates language list to reflect v16
2025-07-01 09:50:27 +02:00
Niels LyngsøandGitHub 629635e0dd observable for detecting if split view is active (#19394)
* observable for detecting if split view is active

* declare splitView manager on content detail workspace

* remove declaration as it was a double
2025-07-01 09:30:55 +02:00
Andy Butland 876d229b7e Merge branch 'v15/dev'
# Conflicts:
#	tests/Umbraco.Tests.UnitTests/Umbraco.Core/Configuration/ContentSettingsExtensionsTests.cs
2025-07-01 08:28:48 +02:00
ad5a18f1ee Fix pagination in Content Delivery API Index Helper (#19606)
* Refactor descendant enumeration in DeliveryApiContentIndexHelper

Improved loop condition to allow for processing of more than 10.000 descendants for indexing.

* Add failing test for original issue.

* Renamed variable for clarity.

---------

Co-authored-by: Brynjar Þorsteinsson <brynjar@vettvangur.is>
Co-authored-by: Andy Butland <abutland73@gmail.com>
2025-07-01 08:23:58 +02:00
Andy ButlandandGitHub cdc62d3020 Aligned jsdoc and inferred function type on UmbCurrentUserContext (#19642)
Aligned jsdoc and inferred function type on UmbCurrentUserContext.
2025-06-30 18:52:25 +02:00
Andy ButlandandGitHub 3e99c2f69d Removes full-width from toggle property editor (#19641)
* Removes full-width from toggle property editor.

* Fixed linting error.
2025-06-30 14:35:34 +00:00
Jacob Overgaard b3d86d5395 build(deps): bump @umbraco-ui/uui from 1.14.0 to 1.14.1 2025-06-30 15:30:32 +02:00
Mads RasmussenandGitHub 50327be6e7 React on late read only guard rules (#19621)
* Observe read-only guard rules in variant selector

Added observation of read-only guard rules in the workspace split view variant selector to ensure read-only cultures are updated when rules change.

* Improve save action to react to read-only rule changes

* remove unused
2025-06-30 15:22:44 +02:00
e135599c96 Fix readonly UI for create document user permission (#19554)
* Update document-workspace.context.ts

* Update src/Umbraco.Web.UI.Client/src/packages/documents/documents/workspace/document-workspace.context.ts

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

* add check for undefined

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-06-30 15:19:46 +02:00
LiliandNiels Lyngsø d6a66bc4d8 V15 Bugfix: Horizontal scrollbar in Media section #19479 (#19541)
fix to stop the scroll bar appearing in the media section.
2025-06-30 14:55:52 +02:00
LiliandGitHub 32a4fbebd5 V15 Bugfix: Horizontal scrollbar in Media section #19479 (#19541)
fix to stop the scroll bar appearing in the media section.
2025-06-30 14:52:35 +02:00
Mads RasmussenandGitHub 283e6b2ff0 Entity type + Entity Unique conditions (#19614)
* Add entity-type and entity-unique condition support

Introduces new condition types for entity-type and entity-unique, including their constants, types, condition implementations, and manifests. Updates exports in core entity modules to include these new features, enabling more granular extension conditions based on entity type and uniqueness.

* register conditions

* add support for oneOf

* fix self imports

* Update manifests.ts

* remove unused
2025-06-30 14:51:21 +02:00
Andy ButlandandGitHub cfbbfa004c Further fix to configuration classes using ISet, resolving regression with custom 404 pages (#19573)
Further fix to configuration classes using ISet, resolving regression with custom 404 pages.
2025-06-30 14:37:04 +02:00
b81d45901b ability to hide block actions (#19626)
Co-authored-by: Mads Rasmussen <madsr@hey.com>
2025-06-30 12:35:18 +00:00
df39026d0d set height to match workspace top bar (#19384)
Co-authored-by: Mads Rasmussen <madsr@hey.com>
2025-06-30 12:32:45 +00:00
HenrikandGitHub 880d84b030 Reduce lookups needed in ConcurrentDictionaries in ContentNavigationServiceBase (#19603)
Reduce lookups needed in ConcurrentDictionaries and sort using List.Sort, make key removal O(1) by using hashsets and avoid duplicates, remove unneeded .ToList() and other minor tweaks
2025-06-30 14:24:04 +02:00
Lee KelleherandGitHub f9c2b9594f Label property-editor, adds UFM template support (#19610)
* Adds `labelTemplate` config to Label editor

* Code tidyup for Label editor element

* Adds "format-bytes" UFM filter

* Renamed "format-bytes" to "bytes"

* Label element tidy-up + copilot amends

* Added `formatBytes` options

* Simplified condition, thanks CodeScene

* Reverted element export
2025-06-30 12:18:02 +00:00
HenrikandGitHub 1f28f1043a Avoid async await Task.FromResult, plus some other minor tweaks (#19597) 2025-06-30 14:15:22 +02:00
Andy Butland e78eee50ed Minor refactor following merge of PR #19617. 2025-06-30 14:08:50 +02:00
Davor ZlotrgandGitHub ba2072c979 fix: json serialization and deserialization for NuCacheSerializerType (#19617)
fix: json serialization and deserialization
2025-06-30 14:07:00 +02:00
Mads RasmussenandGitHub 49b95c1225 Introduce Section Alias condition const (#19633)
* Refactor section conditions into subfolders

Split section condition logic into 'section-alias' and 'section-user-permission' subfolders, each with their own constants, manifests, and types. Updated imports and manifest aggregation to use the new structure for improved modularity and maintainability.

* use const

* fix build

* Refactor section alias condition to use constant

Replaces hardcoded 'Umb.Condition.SectionAlias' strings with the UMB_SECTION_ALIAS_CONDITION_ALIAS constant across all manifests and related files. This improves maintainability and consistency by centralizing the section alias condition reference.
2025-06-30 13:24:00 +02:00
14063a0b89 Add support for file upload property editor within the block list and grid (#18976)
* Fix for https://github.com/umbraco/Umbraco-CMS/issues/18872

* Parsing added for current value

* Build fix.

* Cyclomatic complexity fix

* Resolved breaking change.

* Pass content key.

* Simplified collections.

* Added unit tests to verify behaviour.

* Allow file upload on block list.

* Added unit test verifying added property.

* Added unit test verifying removed property.

* Restored null return for null value fixing failing integration tests.

* Logic has been updated according edge cases

* Logic to copy files from block list items has been added.

* Logic to delete files from block list items on content deletion has been added

* Test fix.

* Refactoring.

* WIP: Resolved breaking changes, minor refactoring.

* Consistently return null over empty, resolving failure in integration test.

* Removed unnecessary code nesting.

* Handle distinct paths.

* Handles clean up of files added via file upload in rich text blocks on delete of the content.

* Update src/Umbraco.Infrastructure/PropertyEditors/FileUploadPropertyEditor.cs

Co-authored-by: Sven Geusens <geusens@gmail.com>

* Fixed build of integration tests project.

* Handled delete of file uploads when deleting a block from an RTE using a file upload property.

* Refactored ensure of property type property populated on rich text values to a common helper extension method.

* Fixed integration tests build.

* Handle create of new file from file upload block in an RTE when the document is copied.

* Fixed failing integration tests.

* Refactored notification handlers relating to file uploads into separate classes.

* Handle nested rich text editor block with file upload when copying content.

* Handle nested rich text editor block with file upload when deleting content.

* Minor refactor.

* Integration test compatibility supressions.

---------

Co-authored-by: Andy Butland <abutland73@gmail.com>
Co-authored-by: Sven Geusens <geusens@gmail.com>
2025-06-30 13:21:10 +02:00
2cb114ffaf Fix check for pending package migration to use the package not the plan name (#19509)
* Fix check for pending package migration to use the package not plan name.

* Cover all package name/identifier permutations and fix the API output for multiple plans

* Adjusted log message to not refer to unattended migrations as migrations may be being run attended.

---------

Co-authored-by: Kenn Jacobsen <kja@umbraco.dk>
2025-06-30 10:20:11 +00:00
Jacob OvergaardandGitHub fb2aad0b1d V16: Adds a state for "forbidden" for entities that return 403 (Access denied) (#19557)
* feat: adds new localization keys for forbidden routes

* feat: ignore all 400, 401, 403, and 404 errors as they are handled by the UI

* feat: adds new elements to show forbidden routes and entities

* feat: adds generic forbidden state to base entities

* feat: injects a forbidden route component to documents

* feat: adds 'forbidden' state to media workspace

* chore: aligns document and media workspaces

* test(mock): adds user configuration endpoint

* test(mock): adds calculate-start-nodes endpoint to users

* test(mock): adds missing endpoint for 'client-credentials'

* feat: clean up old observers on entity errors

* feat: aligns UI for better DX if there is no user

* fix: returns early if there is no user, instead of trying to append properties to the object

* feat: adds 'forbidden' state to members

* feat: adds support for forbidden document blueprints

* feat: allows parent to be undefined as well as null

* feat: forbidden route for members as a state

* chore: simplify language workspace

* test: adds forbidden mock data

* test: adds missing endpoints and a check for forbidden ids
2025-06-30 10:00:11 +01:00
Jacob OvergaardGitHubCopilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
861afde3a0 Fix for code scanning alert no. 1719: Client-side cross-site scripting (#19607)
* Fix for code scanning alert no. 1719: Client-side cross-site scripting

Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>

* feat: uses built-in sanitizeHtml in backoffice

---------

Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
2025-06-30 09:04:50 +01:00
Andy ButlandandGitHub 29b6c2d49e Assert dates in content editing integration tests to millisecond only (#19513)
* Assert dates in content editing integration tests to millisecond only.

* Add date time extension unit tests and refactor to switch statement.

* Removed whitespace.
2025-06-30 07:59:22 +02:00
Andy ButlandandGitHub c223d93a53 Adds XML header docs indicating usage of options on NuCacheSerializerType (#19555)
Adds XML header docs indicating usage of options on NuCacheSerializerType.
2025-06-30 07:53:09 +02:00
Niels LyngsøandGitHub c432f5a66c load more button component and styling (#19622)
* localization

* tree-load-more-button component

* implement

* ability to hide block actions

* Revert "ability to hide block actions"

This reverts commit bf8222f49b.
2025-06-29 18:39:07 +02:00
9cb0557120 Tiptap RTE: A11Y label improvements (#19531)
* a11y: Adds labels to Tiptap RTE toolbar buttons/menus

* a11y: Adds labels to Tiptap toolbar designer actions

* a11y: Adds labels to Tiptap statusbar designer actions

* a11y: Adds label to Overlay Size dropdown

---------

Co-authored-by: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com>
2025-06-27 12:10:12 +00:00
f7aab5e901 Table checkbox/icon alignment (#19615)
* Table checkboxes slotted whitespace

Fixes #19563

* Table markup tidy-up

Resolved the `style` attribute error

* localize texts

---------

Co-authored-by: Mads Rasmussen <madsr@hey.com>
2025-06-27 11:58:00 +00:00
Mads RasmussenandGitHub 75ee1e65d1 Show success/failed state for workspace buttons with additional options (#19535)
* also show success/failed state when button have additional options

* rename method

* clear timeout
2025-06-27 13:38:21 +02:00
b7be95b239 Add folder workspace icon (#19366)
* add slot for icon

* expose icon data

* render icon

* load type for scaffold

* rename

* render icon for media

* add observable for content type icon

* request data in data source

* wire up document scaffolding

* remove unused

* export server data source

* render icon for member

* rename data source to align with other detail sources

* rename data source

* remove unused styling

* remove console log

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

* remove console log

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

* remove console log

* render workspace icon for document type folders

* make folder workspace editor

* use element

* remove const

* use folder-workspace-editor for templating folders

* introduce name write guard manager

* prevent name change of file system folders

* Update script-folder-workspace-editor.element.ts

* make guard optional

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-06-27 13:38:01 +02:00
Laura NetoandGitHub 376a2c8ff6 V16.1: Fix broken content creation when using blueprints (#19518)
Fixes broken content creation based on blueprints

Fixes preset not overriding values in the various createScaffold methods.
2025-06-27 13:36:14 +02:00
Lee KelleherandGitHub 3e891972d0 Tiptap RTE: Adds token for statusbar context (#19530)
Tiptap: adds token for Statusbar context
2025-06-27 13:35:36 +02:00
Lee KelleherandGitHub cdf7b3dbef Tiptap RTE: Toolbar menu active highlighting (#19532)
* Adds optional parameter to Tiptap toolbar item's `isActive`

* Adds `isActive` support to toolbar menus and cascading menus

* Adds `isActive` support to the font menus

* Adds `isActive` support to the table menu

+ UI/CSS tweak

* Adds `isActive` support to the style menu API

+ refactored the commands

* Improves cascading menu popover closing

it previously didn't close the menu when an action was clicked.
2025-06-27 13:33:17 +02:00
Mads RasmussenandGitHub ede906e152 DocumentUrlDataResolver: Use UMB_VARIANT_CONTEXT instead of UMB_PROPERTY_DATASET_CONTEXT (#19537)
* change to variant context

* remove unused
2025-06-27 13:32:22 +02:00
5cafa599fa Remove trash success notifications (#19534)
* remove trash notifications

* Updated tests so we no longer use the notification for moving to recycle bin

---------

Co-authored-by: Andreas Zerbst <andr317c@live.dk>
2025-06-27 13:31:08 +02:00
e3c52afbbb v16 QA Remove smoke tag from flaky test (#19611)
* Remove from smoke tag

* Update tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithImageCropper.spec.ts

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

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-06-26 13:10:58 +02:00
Kenn JacobsenandGitHub c61fc7419c Cherry picked #19540 to V16 (and fixed changed signatures) (#19592) 2025-06-26 09:16:49 +02:00
67106f0813 Add missing media exports (#19585)
* Add missing media exports

Fixes #19560 and #19561

* Update src/Umbraco.Web.UI.Client/src/packages/media/media/index.ts

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

---------

Co-authored-by: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-06-24 12:17:40 +00:00
55506bac3a Simplify creating content from a blueprint programmatically (#19528)
* Rename `IContentService.CreateContentFromBlueprint` to `CreateBlueprintFromContent`

In reality, this method is used by the core to create a blueprint from content, and not the other way around, which doesn't need new ids. This was causing confusion, so the old name has been marked as deprecated in favor of the new name. If developers want to create content from blueprints they should use `IContentBlueprintEditingService.GetScaffoldedAsync()` instead, which is what is used by the management api.

* Added integration tests to verify that new block ids are generated when creating content from a blueprint

* Return copy of the blueprint in `ContentBlueprintEditingService.GetScaffoldedAsync` instead of the blueprint itself

* Update CreateContentFromBlueprint xml docs to mention both replacement methods

* Fix tests for rich text blocks

* Small re-organization

* Adjusted tests that were still referencing `ContentService.CreateContentFromBlueprint`

* Add default implementation to new CreateBlueprintFromContent method

* Update tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentBlueprintEditingServiceTests.GetScaffold.cs

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

---------

Co-authored-by: Andy Butland <abutland73@gmail.com>
2025-06-24 13:43:34 +02:00
Jacob OvergaardandGitHub b41eecf58c V16: Document shows blank page when clicking an active link (#19553)
* fix: if no workspace views are found at all, show a not found page

* fix: rather than redirecting to the first available tab, which may not always be available on secondary routing, let the router display the first tab on an empty url

this mirrors how workspace views are displayed in umb-workspace-editor
2025-06-24 09:45:31 +01:00
Lee KelleherandGitHub 0a7a92287b Variant workspace breadcrumb item parentheses (#19599)
* Refactored breadcrumb variant name

The parentheses will be added to unnamed variant ancestor items.

* Adds `last-item` attribute

(for semantics)

* Imports tidy-up

* Refactored `#getHref` to early exit for `.isFolder`

Saves on the string allocation.
2025-06-24 10:10:02 +02:00
6237ddca89 Improves the layout on the create blueprint dialog (#19556)
* Improves the layout on the create blueprint dialog.

* localize texts

---------

Co-authored-by: Mads Rasmussen <madsr@hey.com>
2025-06-24 08:08:09 +00:00
Mads RasmussenandGitHub ef453ad62e Docs: Collection example (#19593)
* add basic collection example

* add card view example

* update example readme

* Add workspace view example with collection
2025-06-24 08:31:26 +01:00
Nhu DinhandGitHub e6791246e4 V16 QA Added acceptance tests for Webhook (#19545)
* Added tests for webhook

* Added tests for webhook trigger

* Bumped version

* Make all Webhook tests run in the pipeline

* Fixed comment

* Reverted npm command
2025-06-17 08:55:30 +00:00
Andy ButlandandGitHub 50272f7401 Removed duplicate/obsolete code fixing issue with document move permission checks (#19552)
Removed duplicate/obsolete code fixing issue with document move permission checks.
2025-06-13 14:41:08 +02:00
Nhu DinhandGitHub 9964e0b8ec V16 QA Added acceptance tests for granular property value permission (#19458)
* Updated due to test helper changes

* Updated user group tests due to api helper changes

* Updated tests for user group default configuration due to UI changes

* Added tests for document property value permission

* Added tests for document property value permission in content with block

* Bumped version

* Make specific tests run in the pipeline

* Added skip tag and issue link for the failing tests

* Added tests for granular property value permission

* Fixed comment

* Bumped version

* Bumped version

* Fixed comments

* Bumped version and reverted npm command

* Make all tests for user group permission run in the pipeline

* Updated smokeTest command

* Fixed comments

* Reverted npm command
2025-06-13 11:13:28 +07:00
Andy ButlandandGitHub 9d11c76ea1 Added start-up logging of document URL caching (#19538)
Added startup logging of document URL caching.
2025-06-12 22:20:44 +02:00
Andy Butland da9c6e24ce Removed unnecessary compatibility suppressions file from integration tests. 2025-06-12 13:50:29 +02:00
Jacob OvergaardandGitHub cd72dbe4ea V16: Retry requests after timeout (#19495)
* feat: fix a small-ish nitpick where extensions would reload after login

this could potentially try to re-register all private extensions after each auth signal, which is being prevented anyway because of duplicate aliases, but still nice to remove and not have to listen to

* feat: align login UI extension load with backoffice, i.e. wait for external load before registering core extensions

* build(deps): bump @hey-api to newest and re-generate client

* chore: adds extra error logging

* feat: adds retry logic to the api interceptor

* feat: warn about incomplete actions

* fix: the body was already plain text, but we need to ensure the headers say so as well

* feat: warns the user when actions could not be completed

* build(deps): update @hey-api/client-fetch

* chore: generate new api

* feat: simplify error handling to just UmbApiError and UmbCancelError

* feat: moves error notifications from interceptors into tryExecute, so you more easily can opt out of it and everything is gathered in one place

* feat: recreate responses with correct 'status' and 'statusText'

* build: stop dotnet processes after debug session

* feat: extrapolate common logic into helper method to create responses

* feat: returns a UmbProblemDetails like object on interceptors to be handled by tryExecute

* chore: deprecates duplicate, outdated UmbProblemDetails interface and type guard

* feat: uses the 'title' of the problem details object to convey the main message

* chore: 401 and 403 uses their own interceptors

* feat: show no notification if 401

* feat: uses the real request method and url (instead of the template placeholders) to tell the user what did not succeed

* feat: retry requests with no timeout/race

* feat: throttle and delay signals and disallow them from being updated from the outside

* chore: adds more logging to timeouts

* chore: optimise imports

* test: ignores any test files left in node_modules folder

* feat: uses auditTime to wait a bit before showing the timeout screen

* feat: adds 404 handling to error interceptor

* chore: cleans up after response modification

* feat: preserve only a few headers

this mimicks the v15 behavior

* feat: lets the UI handle 404 errors instead of notifying directly

* test: uses create action menu option instead to find the correct locator, and skips a seemingly unnecessary timeout
2025-06-12 12:24:11 +01:00
e89e18f5ba V16: Item and Detail Base Repository should use correct typings for return types (#19447)
* fix: add a catcher to most `asPromise` for stores to prevent cascading errors

* fix: remove conditional instances - they should be able to be undefined

* fix: check for missing store and extract UmbProblemDetails

* fix: only append data if no error

* fix: adds error handling to missing stores and to extract the ProblemDetails object

* revert commit

* fix: ignore errors completely instead of unsetting stores

* revert commit

* chore: cleanup imports

* fix: do not unset store

* stop observation in a proper way

* stop observation of for document-user-permissions

* check for manager twice

* save action

* save action optional

* fix: ensure the right types are used for base stores

---------

Co-authored-by: Niels Lyngsø <niels.lyngso@gmail.com>
2025-06-12 11:59:11 +01:00
Andy Butland cfcb708d26 Merge branch 'release/16.0' and enable package validation 2025-06-12 08:18:13 +02:00
Laura NetoandGitHub 9a96ebf812 Replace keys in Rich Text Editor blocks on clone operations (#19526)
* Regenerate keys in RTE blocks on clone operations

This was already present for BlockList and BlockGrid, but not Blocks in RTE.

* Small adjustment from code review
2025-06-12 07:03:49 +02:00
Lee KelleherandGitHub 919b65ea19 Tiptap RTE: Style Menu action toggles (#19520)
* Tiptap style menu toggles (for classes and IDs)

Fixes #19244

* Tiptap style menu toggles (for font/color)

Fixes #19508

* Tiptap "Clear Formatting" remove classes and styles

* Tiptap font sizes, removes trailing semicolon

as the API handles the delimiter

* Tiptap global attrs: adds set/unset styles commands
2025-06-11 09:21:21 +02:00
Andy Butland cc2f1df78e Merge branch 'release/15.4.3' into v15/dev
# Conflicts:
#	version.json
2025-06-10 11:43:50 +02:00
Andy ButlandandCopilot 9b2fd1253b Optimize initialization of document URLs on start-up (#19498)
* Optimize initialization of document URLs on startup.

* Apply suggestions from code review

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

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-06-10 10:31:26 +02:00
Andy ButlandandGitHub fe7f0558c1 Update template to reference latest LTS version (#19517)
Update template to reference latest LTS version.
2025-06-10 10:31:10 +02:00
Andy Butland 024697f3bf Bumped version to 15.4.3 2025-06-10 10:28:55 +02:00
8d2ff6f92a Optimize initialization of document URLs on start-up (#19498)
* Optimize initialization of document URLs on startup.

* Apply suggestions from code review

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

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-06-10 10:28:45 +02:00
Lee KelleherandGitHub 69224cb53c Tiptap RTE: Toolbar/statusbar config initial value state (#19514)
Tiptap configuration waits until initialized to set value

Fixes #19009
2025-06-10 08:54:33 +01:00
Andy Butland 89b2120c46 Obsoleted an unused constant ListViewMembers. 2025-06-06 13:49:52 +02:00
Andy ButlandandGitHub 6c769d6024 Adds attribute to ensure management API requests are never cached in the browser or via a CDN (#19496)
Adds attribute to ensure management API requests are never cached in the browser or via a CDN.
2025-06-06 10:41:13 +00:00
Andy Butland 67ab06715f Merge branch 'release/16.0' 2025-06-06 10:49:21 +02:00
Jacob Overgaard b3ac82baf8 Merge branch 'release/16.0' 2025-06-05 15:44:23 +02:00
Niels LyngsøandGitHub 7f51e1ad1b Manifest Viewer to support number and boolean (#19478)
support number and boolean
2025-06-05 13:39:16 +00:00
Andreas ZerbstandGitHub c7dcbfec60 V16 QA Publish Console Error Logs (#19417)
* Updated configuration

* Updated pipeline

* Updated nightly pipeline

* Bumped version

* updated if statement
2025-06-05 11:43:00 +00:00
Andreas ZerbstandGitHub 3f57da5579 V16 QA updated notification acceptance tests (#19469)
* Updated block grid tests

* Updated notifications in tests

* Updated tests

* Added testIdAttribute

* Bumped version of testHelpers

* Added waits after creation

* Updated more tests related to notifications

* Bumped version

* Cleaned up

* updated tests

* Bumped version

* Updated tests

* bumped version
2025-06-05 09:13:16 +00:00
Karl-Johan SjögrenandGitHub ce2756f399 Fixed JSDoc comment for ManifestBase.weight (#19488) 2025-06-05 09:45:14 +02:00
Jacob Overgaard 6cfba9b406 Merge branch 'release/16.0' 2025-06-04 10:01:45 +02:00
dependabot[bot]andJacob Overgaard 29b98ba862 Bump tar-fs from 3.0.8 to 3.0.9 in /src/Umbraco.Web.UI.Client
Bumps [tar-fs](https://github.com/mafintosh/tar-fs) from 3.0.8 to 3.0.9.
- [Commits](https://github.com/mafintosh/tar-fs/compare/v3.0.8...v3.0.9)

---
updated-dependencies:
- dependency-name: tar-fs
  dependency-version: 3.0.9
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-06-03 13:41:10 +02:00
971ca17b5f Add unit test verifying dockerfile aligns with current target framework (#19445)
* Add unit test verifying dockerfile aligns with current target framework.

* Apply suggestions from code review

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

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-06-03 12:53:37 +02:00
Andy Butland d8b76575ca Merge branch 'v15/dev' of https://github.com/umbraco/Umbraco-CMS into v15/dev 2025-06-03 07:04:56 +02:00
Andy Butland 5b00312158 Merge branch 'release/15.4.2' into v15/dev
# Conflicts:
#	version.json
2025-06-03 07:04:43 +02:00
Andy ButlandandGitHub d920e93d1e Merge commit from fork 2025-06-03 05:21:11 +02:00
Sven GeusensandGitHub d2a0cba933 V16 cherry pick of member partial cache invalidator see #19314 (#19459)
* v16 cherry pick of member partial cache invalidator see #19314

# Resolved merge conflic in src/Umbraco.Core/Cache/Refreshers/Implement/MemberCacheRefresher.cs

* Take nullmember cacheitems into account
2025-06-02 14:52:38 +02:00
Nhu DinhandGitHub 5a56541aa5 V16 Added acceptance tests for document property value permission (#19385)
* Updated due to test helper changes

* Updated user group tests due to api helper changes

* Updated tests for user group default configuration due to UI changes

* Added tests for document property value permission

* Added tests for document property value permission in content with block

* Bumped version

* Make specific tests run in the pipeline

* Added skip tag and issue link for the failing tests

* Fixed comment

* Bumped version

* Fixed comments

* Bumped version and reverted npm command
2025-06-02 16:21:09 +07:00
Jacob Overgaard 93873c378e Merge branch 'release/16.0' 2025-05-30 15:44:22 +02:00
Richard OckerbyandGitHub ba6a0fcfe0 Update references to Umbraco User model (Extension Template) (#19349)
Update references to Umbraco User model

In the template dashboard, update the type reference for the umbraco user
2025-05-30 14:31:17 +02:00
Laura NetoandGitHub b403518986 Use the proper umb-input-date events in the log viewer date range element (#19451)
Fixes #19382 by using proper umb-input-date events

The log viewer date range input was changed from using `input` to `umb-input-date`, but the event handlers weren't updated accordingly.
2025-05-30 10:44:52 +00:00
Andy ButlandandCopilot fd3808ae38 Ensure tag operations are case insensitive on insert across database types (#19439)
* Ensure tag operations are case insensitve on insert across database types.

* Ensure tags provided in a single property are case insensitively distinct when saving the tags and relationships.

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

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

* Handle case sensitivity on insert with tag groups too.

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-05-30 10:00:25 +02:00
Laura NetoandGitHub c4be7842a7 Sort available composition folders (#19373)
* Sort available composition folders

* Optimizations
2025-05-29 09:08:55 +02:00
Andy Butland 1726180b06 Merge branch 'release/16.0'
# Conflicts:
#	version.json
2025-05-29 07:59:05 +02:00
Andy Butland 3b84fe3557 Merge remote-tracking branch 'origin/v15/dev' into v16/task/merge-from-15
# Conflicts:
#	src/Umbraco.Core/PublishedCache/IPublishedCache.cs
2025-05-29 07:57:00 +02:00
Jacob Overgaard d76293bacc Merge remote-tracking branch 'origin/release/16.0' 2025-05-28 17:07:59 +02:00
179caacf58 Throw if attempting to use the default unique media path scheme with version 7 GUIDs (#19419)
* Thow if attempting to use the default unique media path scheme with version 7 GUIDs.

* Expanded unittests, fixed null params, chose a better exception

* Use parameters in test.

---------

Co-authored-by: Migaroez <geusens@gmail.com>
2025-05-28 12:49:36 +00:00
Laura NetoandGitHub 933fa544d7 Translations for pt (pt-PT) (#19435)
* Added translations for `pt-PT` based on the existing `en` file

* Removed translations from `pt-BR` that are the same in `pt` or not translated. Other small adjustments.

* Replace all `ligação` with `link`, as it is more commonly used

* Small typo fixes in pt-BR
2025-05-28 11:40:00 +02:00
b019f0f849 Do not set icon color if the item is selected (#19404)
* Do not set icon color if the item is selected

* Added helper method for icon version to render.

* Fixed naming of protected helper method.

* Move further logic into helper method.

---------

Co-authored-by: Andy Butland <abutland73@gmail.com>
2025-05-28 08:53:44 +00:00
Niels LyngsøandGitHub 7346a14d86 update next icon (#19424) 2025-05-27 14:54:07 +02:00
Rowan BottemaandAndy Butland ecfaee3b04 Lock appropriate tree for media operations (#19422)
The MediaService currently locks the ContentTree for GetPagedOfType(s) operations, but it's querying the MediaTree. This ensures we lock the correct tree.
2025-05-27 06:59:21 +02:00
Niels Lyngsø 80183964bb Merge branch 'release/16.0' 2025-05-26 21:18:51 +02:00
Andy Butland 445652d7f4 Merge branch 'release/16.0'
# Conflicts:
#	tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithMultiURLPicker.spec.ts
#	version.json
2025-05-26 10:26:15 +02:00
Andy ButlandandGitHub 13e368ca28 Cleaned up constructors, regions and variables in NewDefaultUrlProvider (#19403)
Cleaned up constructors, regions and variables in NewDefaultUrlProvider.
2025-05-26 07:11:53 +02:00
Andy Butland e6967a41cd Removes unnecessary newlines from rich text as JSON delivery API output (#19391)
* Removes unnecessary newlines from rich text as JSON delivery API output.

* Fix case from PR feedback.
# Conflicts:
#	src/Umbraco.Infrastructure/DeliveryApi/ApiRichTextElementParser.cs
#	tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/RichTextParserTests.cs
2025-05-23 12:28:18 +02:00
MoleandGitHub feba51a49d Use correct service name in obsoletion method (#19398)
Use correct service in obsoletion method
2025-05-23 09:46:16 +00:00
Andy Butland b87ae6ce46 Fix issue with requests to delivery API by path where URL segment contains special characters (#19390) 2025-05-23 11:14:33 +02:00
Andreas ZerbstandGitHub 3db0ec7efc V16 Updated nightly E2E pipeline to run correct test scripts (#19378)
* Set default to true, so we run the SQlite tests aswell

* Updated test commands to run correct tests

* Disabled Sqlite tests

* Skip flaky test
2025-05-22 11:24:46 +02:00
0f53ba8a18 fix: check for NullRepresentationInCache in AppCacheExtensions (#19350)
* fix: add appcache null check

* Moved constant into standard location.
Removed now unnecessary comment.

---------

Co-authored-by: Andy Butland <abutland73@gmail.com>
2025-05-22 08:45:30 +02:00
Niels Lyngsø 45593c6311 Merge branch 'release/16.0'
# Conflicts:
#	version.json
2025-05-21 11:17:47 +02:00
Laura NetoandGitHub 225fed5f72 Handle user id 0 (Unknown/System) when building content version response (#19361)
Handle user id 0 (Unknown/System) when building content version response model

`IUserIdKeyResolver.GetAsync` throws an exception when a user is not found.
As user 0 does not really exist, the exception was being thrown.
We now handle this scenario to return an empty reference.
2025-05-20 17:01:03 +02:00
Andy Butland 419625a919 Optimize the member save as part of the member login process, by-passing locking and audit steps and handling only the expected update properties (#19308) 2025-05-20 15:58:27 +01:00
Andy Butland 853c1acf7e Bumped version to 15.4.2 2025-05-20 13:10:02 +01:00
Andreas ZerbstandGitHub b1e447d0c5 Merge pull request #19371 from umbraco/v16/merge-16-release-to-main
Merged from release/16.0 to main branch
2025-05-20 13:15:49 +02:00
Andreas Zerbst df11e438f4 Merge remote-tracking branch 'origin/release/16.0' into v16/merge-16-release-to-main
# Conflicts:
#	build/nightly-E2E-test-pipelines.yml
#	src/Umbraco.Web.UI.Client/eslint.config.js
#	src/Umbraco.Web.UI.Client/package-lock.json
#	src/Umbraco.Web.UI.Client/package.json
#	tests/Umbraco.Tests.AcceptanceTest/package-lock.json
#	tests/Umbraco.Tests.AcceptanceTest/package.json
#	tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/BlockGrid/VariantBlockGrid.spec.ts
#	tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/BlockList/VariantBlockList.spec.ts
#	tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentInfoTab.spec.ts
#	tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithCheckboxList.spec.ts
#	tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithDropdown.spec.ts
#	tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithMediaPicker.spec.ts
#	tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithRadiobox.spec.ts
#	tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithTextarea.spec.ts
#	tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithTextstring.spec.ts
#	tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/RedirectManagement.spec.ts
#	tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/RichTextEditor/ContentWithTiptap.spec.ts
#	tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/RichTextEditor/VariantTipTapBlocks.spec.ts
#	tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Dictionary/Dictionary.spec.ts
#	version.json
2025-05-20 12:25:14 +02:00
7572ea350b Feature: Content Workspace Icon (#19292)
* add slot for icon

* expose icon data

* render icon

* load type for scaffold

* rename

* render icon for media

* add observable for content type icon

* request data in data source

* wire up document scaffolding

* remove unused

* export server data source

* render icon for member

* rename data source to align with other detail sources

* rename data source

* remove unused styling

* remove console log

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

* remove console log

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

* remove console log

* Update detail-repository-base.ts

* Update document-workspace-split-view.element.ts

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-05-20 11:12:19 +02:00
Karl-Johan SjögrenandGitHub ec6a38e052 Fix for invalid state in JsonBlockValueConverter when an unused layout has a nested array (#19363)
* Fix for invalid state in JsonBlockValueConverter when an unused layout has a nested array

* Improved comments as suggested by copilot review, also fixed code style miss

* Added check for malformed JSON with more closing array tokens then opening tokens
2025-05-20 10:07:50 +01:00
Nhu DinhandGitHub ea8ec4e375 V16 QA Updated E2E nightly pipeline (#19355)
Disabled SQLite tests and made it run against the release/16.0 branch
2025-05-20 11:31:14 +07:00
Jacob Overgaard ceab77d911 fix: remove direct imports (they are already imported by the index barrel) 2025-05-16 14:18:15 +02:00
e62e55d1c0 Media Picker Modal File Previews (#19335)
* Ordered `@property` setters/getters

* JSDocs + comments

* Removed unneeded CSS

* Markup code formatting

* Hides "Reset focal point" button

when focal point has default value

* Image Cropper field element: adds active state to crops

* Image Cropper Editor Field element: reduced the markup and styles

Removing duplications from inherited class

* Removed unused code from Image Cropper Focus Setter element

* Big refactor of Image Cropper Editor modal

to support File Upload Previews

* Added `<umb-file-upload-preview>`

to handle the logic of rendering the relevant `fileUploadPreview` extension.

* Refactored SVG File Upload Preview component

Removes the `<uui-card-media>` container.
Controversially removes the link, but this was inconsistent with other file previews.

* Refactored General File Upload Preview component

Removes the `<uui-card-media>` container.
Controversially removes the link, but this was inconsistent with other file previews.

* Refactored Image File Upload Preview component

Removes the `<uui-card-media>` container.
Controversially removes the link, but this was inconsistent with other file previews.

* Refactored Audio and Video File Upload Preview component

To align code with the other file previews.

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

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

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com>
2025-05-16 10:29:09 +00:00
AbdulazizandGitHub 76fcb4ade2 Change CONTRIBUTING.md to fit the new github style and also the branc… (#19246)
Change CONTRIBUTING.md to fit the new github style and also the branch to fork as there is no more contrib branch
2025-05-16 09:05:04 +02:00
c20b0ab3c9 Fix localiztion for buttons in pagination (#19006)
Co-authored-by: Lan Nguyen Thuy <lnt@umbraco.dk>
2025-05-15 22:31:28 +02:00
Laura NetoandGitHub b42888dffa Cannot view audit logs when they contain entries with user id 0 (#19263) 2025-05-12 12:31:16 +02:00
Jacob Overgaard 2b00f15d1a build: prevents public releases that matches the main branch 2025-05-12 10:33:41 +02:00
Nhu DinhandGitHub 5d9d94a0e8 V16 QA Added acceptance tests for publishing with descendants (#19262) 2025-05-09 07:27:02 +00:00
Niels LyngsøandGitHub a5d32336fc Fix: Ensure minimum height for Block List Custom Views (#18398) (#19253) 2025-05-08 11:04:03 +02:00
Lee KelleherandGitHub 712cbe84c1 Member Type Root Collection View / Workspace (#19271) 2025-05-08 10:22:10 +02:00
Mads RasmussenandJacob Overgaard d1f0437daf Update eslint.config.js 2025-05-08 08:36:57 +02:00
Nikolaj Geisle a31923c1e2 Merge branch 'release/16.0'
# Conflicts:
#	src/Umbraco.Core/DependencyInjection/UmbracoBuilder.Collections.cs
2025-05-07 18:24:26 +02:00
Jacob Overgaard f42328e46f trigger the nightly builds on the main branch 2025-05-07 12:02:31 +02:00
Nhu DinhandGitHub 4d7641be97 V16 QA Updated the nightly test pipeline to run against the main branch (#19261) 2025-05-07 10:09:28 +02:00
41ca028023 Normalize webhook payloads (#19110)
Co-authored-by: Andy Butland <abutland73@gmail.com>
Co-authored-by: Kenn Jacobsen <kja@umbraco.dk>
2025-05-06 14:01:11 +02:00
dependabot[bot]andJacob Overgaard cdadd6c29d Bump vite from 6.2.6 to 6.3.5 in /src/Umbraco.Web.UI.Login
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 6.2.6 to 6.3.5.
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v6.3.5/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  dependency-version: 6.3.5
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-05-06 10:34:38 +02:00
DitteKKoustrupandGitHub 489322adac Create Localization Contributing Documentation (#18412) 2025-05-05 15:46:37 +02:00
Jacob Overgaard 2d7037df4b docs: updates branch recommendation 2025-05-05 15:27:38 +02:00
Jacob Overgaard 44552ccf75 build(dev): update all github workflows with new default branch name 2025-05-05 15:26:51 +02:00
dependabot[bot]andJacob Overgaard 6f4a8a2041 Bump vite from 6.3.2 to 6.3.5 in /src/Umbraco.Web.UI.Client
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 6.3.2 to 6.3.5.
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v6.3.5/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  dependency-version: 6.3.5
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-05-05 15:02:06 +02:00
Jacob Overgaard b2e80865b8 Merge branch 'v16/dev' into contrib 2025-05-05 14:16:22 +02:00
Jacob Overgaard 641e549bae build: bump version to 16.1.0-rc 2025-05-05 14:15:40 +02:00
Jacob Overgaard 4dd7ce1a73 Merge branch 'release/16.0' into v16/dev 2025-05-05 14:14:49 +02:00
Sebastiaan Janssen 358ab7b205 Merge branch 'v16/dev' into contrib 2025-05-05 14:12:22 +02:00
Sebastiaan Janssen 7e647c6b38 Merge branch 'contrib' of https://github.com/umbraco/Umbraco-CMS into contrib
# Conflicts:
#	src/Umbraco.Web.UI.Client/package-lock.json
#	src/Umbraco.Web.UI.Client/package.json
#	src/Umbraco.Web.UI.Login/package-lock.json
2025-05-05 14:11:45 +02:00
6ce4fbfc0c V16 QA Fixed nightly test pipeline (#19226)
* Use Windows instead

* Updated the build stage to align with the azure pipelines yml

---------

Co-authored-by: Andreas Zerbst <andr317c@live.dk>
2025-05-05 10:13:50 +00:00
Andy Butland f39094fcea Merge remote-tracking branch 'origin/v15/dev' into v16/dev
# Conflicts:
#	Directory.Packages.props
2025-05-05 10:36:43 +02:00
Markus JohanssonandGitHub 12049e9c03 Tiptap cannot set the top position of the toolbar (#19087)
* Fixes #19086 by making top position a variable

* #19086 Added JSDoc for cssprops
2025-05-05 07:55:52 +02:00
d83e9f736d Changed the Image crop UI component to fit the new Backoffice UI design (#19204)
* Changed the Image crop UI component to fit the new Backoffice UI

* style adjustments

---------

Co-authored-by: Niels Lyngsø <niels.lyngso@gmail.com>
2025-05-02 06:49:25 +00:00
dependabot[bot]andJacob Overgaard 1c1035e1ea Bump vite from 6.2.6 to 6.3.4 in /src/Umbraco.Web.UI.Login
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 6.2.6 to 6.3.4.
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v6.3.4/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  dependency-version: 6.3.4
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-05-02 08:34:23 +02:00
dependabot[bot]andJacob Overgaard 03523ad90b Bump vite from 6.2.6 to 6.3.4 in /src/Umbraco.Web.UI.Client
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 6.2.6 to 6.3.4.
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v6.3.4/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  dependency-version: 6.3.4
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-05-02 08:30:42 +02:00
Nathaniel NunesandGitHub abf068934d add overflow:visible to allow the workspace action menu to be highlighted (#18391)
* add overflow:visible to allow the workspace action menu to be highlighted when tabbed into
2025-05-02 01:16:27 +02:00
Niels Lyngsø 057fd05635 Merge branch 'release/16.0' into v16/dev
# Conflicts:
#	tests/Umbraco.Tests.AcceptanceTest/package-lock.json
#	tests/Umbraco.Tests.AcceptanceTest/package.json
2025-05-01 22:13:01 +02:00
Niels Lyngsø dc4355cbe2 use FormControl value property 2025-05-01 20:59:06 +02:00
mole 8e5f3c1cdf Merge remote-tracking branch 'origin/release/16.0' into v16/dev 2025-04-30 11:12:35 +02:00
Andreas ZerbstandGitHub acdaee6ad8 V16 QA update failing nightly tests (#19190)
* Fixed tests

* More updates for tests

* Bumped version of testhelpers

* Fixed notifications in tests

* Last fixes
2025-04-30 08:39:08 +00:00
Jacob Overgaard f7a2569506 Merge branch 'v15/dev' into v16/dev 2025-04-29 15:53:57 +02:00
Jacob Overgaard 581798f097 Merge branch 'v15/dev' into v16/dev 2025-04-29 13:06:49 +02:00
Andy ButlandandGitHub 5469d9841b Allow deselection of color picker property. (#19174) 2025-04-28 15:50:03 +02:00
991 changed files with 24153 additions and 12191 deletions
+18 -18
View File
@@ -1,8 +1,8 @@
# Contributing to Umbraco CMS
👍🎉 First off, thanks for taking the time to contribute! 🎉👍
👍🎉 First of all, thanks for taking the time to contribute! 🎉👍
These contribution guidelines are mostly just that - guidelines, not rules. This is what we've found to work best over the years, but if you choose to ignore them, we still love you! 💖 Use your best judgement, and feel free to propose changes to this document in a pull request.
These contribution guidelines are mostly just that - guidelines, not rules. This is what we've found to work best over the years, but if you choose to ignore them, we still love you! 💖 Use your best judgment, and feel free to propose changes to this document in a pull request.
We have a guide on [what to consider before you start](contributing-before-you-start.md) and more detailed guides at the end of this article.
@@ -12,51 +12,51 @@ This guide describes each step to make your first contribution:
1. **Fork**
Create a fork of [`Umbraco-CMS` on GitHub](https://github.com/umbraco/Umbraco-CMS)
Create a fork of [`Umbraco-CMS` on GitHub](https://github.com/umbraco/Umbraco-CMS)
![Fork the repository](img/forkrepository.png)
![Fork the repository](img/forkrepositorynew.png)
2. **Clone**
When GitHub has created your fork, you can clone it in your favorite Git tool or on the command line with `git clone https://github.com/[YourUsername]/Umbraco-CMS`.
When GitHub has created your fork, you can clone it in your favorite Git tool or on the command line with `git clone https://github.com/[YourUsername]/Umbraco-CMS`.
![Clone the fork](img/clonefork.png)
![Clone the fork](img/cloneforknew.png)
3. **Switch to the correct branch**
Switch to the `contrib` branch
Switch to the `main` branch
4. **Branch out**
Create a new branch based on `contrib` and name it after the issue you're fixing, For example: `v15/bugfix/18132-rte-tinymce-onchange-value-check`.
Create a new branch based on `main` and name it after the issue you're fixing. For example: `v15/bugfix/18132-rte-tinymce-onchange-value-check`.
Please follow this format for branches: `v{major}/{feature|bugfix|task}/{issue}-{description}`.
Please follow this format for branches: `v{major}/{feature|bugfix|task}/{issue}-{description}`.
This is a development branch for the particular issue you're working on, in this case a bug-fix for issue number `18132` that affects Umbraco v.15.
This is a development branch for the particular issue you're working on, in this case, a bug-fix for issue number `18132` that affects Umbraco v.15.
Don't commit to `contrib`, create a new branch first.
Don't commit to `main`, create a new branch first.
5. **Build or run a Development Server**
You can build or run a Development Server with any IDE that supports DotNet or the command line.
You can build or run a Development Server with any IDE that supports .NET or the command line.
Read [Build or run a Development Server](BUILD.md) for the right approach to your needs.
Read [Build or run a Development Server](BUILD.md) for the right approach to your needs.
6. **Change**
Make your changes, experiment, have fun, explore and learn, and don't be afraid. We welcome all contributions and will [happily give feedback](contributing-first-issue.md#questions).
Make your changes, experiment, have fun, explore and learn, and don't be afraid. We welcome all contributions and will [happily give feedback](contributing-first-issue.md#questions).
7. **Commit and push**
Done? Yay! 🎉
Done? Yay! 🎉
Remember to commit to your branch. When it's ready push the changes to your fork on GitHub.
Remember to commit to your branch. When it's ready, push the changes to your fork on GitHub.
8. **Create pull request**
On GitHub, in your forked repository (`https://github.com/[YourUsername]/Umbraco-CMS`) you will see a banner saying that you pushed a new branch and a button to make a pull request. Tap the button and follow the instuctions.
On GitHub, in your forked repository (`https://github.com/[YourUsername]/Umbraco-CMS`), you will see a banner saying that you pushed a new branch and a button to make a pull request. Tap the button and follow the instructions.
Want to read further? [Creating a pull request and what happens next](contributing-creating-a-pr.md).
Would you like to read further? [Creating a pull request and what happens next](contributing-creating-a-pr.md).
## Further contribution guides
+1 -1
View File
@@ -114,7 +114,7 @@ To declare the Published Cache Status Dashboard as a new manifest, we need to ad
},
conditions: [
{
alias: 'Umb.Condition.SectionAlias',
alias: UMB_SECTION_ALIAS_CONDITION_ALIAS,
match: 'Umb.Section.Settings',
},
],
+223
View File
@@ -0,0 +1,223 @@
# **Contributing to Localization in the Backoffice**
Do you want to help keep our translations accurate and up to standard? 🌍✨
Your input makes a real difference! By reviewing, refining, or suggesting improvements, you ensure that our translations remain clear, consistent, and user-friendly for everyone.
## **How Can I Contribute?**
To contribute to localization in the Backoffice, follow this step-by-step guide:
### **1. Change the Language in Backoffice**
1. Open the Backoffice, click on your profile icon in the top-right corner, and select "Edit."
![Menubar profile image](/.github/img/contributing/ProfileImage.png "Menubar profile image")
![Edit button inside profile](/.github/img/contributing/editBtn.png "Edit button inside profile")
2. Under "UI Culture," select the language you want to review from the dropdown menu.
![Dropdown of languages in Umbraco](/.github/img/contributing/uiCulture.png "Dropdown of languages in Umbraco")
### **2. Find a Translation Error**
1. Navigate through the Backoffice and check if everything is translated correctly.
2. When you find a translation error, right-click on it and select "Inspect."
3. Look for the nearest element that starts with `umb-` and has a name indicating something specific to the given location.
**Example:**
* The closest parent element should be specific, such as `umb-document-type-workspace-view-settings` instead of a generic element like `umb-property-layout.`
### **3. Find the Code in VS Code**
1. Open VS Code and search for the nearest `umb-` element you identified.
![Search for nearest umb element in code](/.github/img/contributing/searchInVsCode.png "Search for nearest umb element in code")
2. Scroll down to find `render() {` and look for the element label that needs updating.
![Find render and label in code](/.github/img/contributing/renderCode.png "Find render and label in code")
3. If the label is hardcoded, it must be updated.
**Example:**
`label="Vary by culture"`
### **4. Find the Correct Translation**
1. Open the `en.ts` or `en-us.ts` file and search for relevant keywords. \
\
**Example:**
* If the text is "Vary by culture," search for `vary`, `culture`, or `Vary by culture`.
2. Once you find the translation, take the element name and search for it in the target language file (e.g., `da-dk.ts` for Danish).
![Search for translation in language files](/.github/img/contributing/searchingThroughLanguageFiles.png "Search for translation in language files")
3. If a translation exists, insert it into the label element found earlier.
### **5. Insert the Translation**
To display the new translation correctly, insert the following code inside the label element:
`${this.localize.term('action_key')}`
Replace `action_key` with the correct translation key.
**Example:**
`${this.localize.term('contentTypeEditor_allowVaryByCulture')}`
![Localization code snippet](/.github/img/contributing/localizationCodeSnippetInCode.png "Localization code snippet")
Save the changes and return to the Backoffice to see the update.
![Changes in backoffice after changes in code](/.github/img/contributing/changedBackofficeAfterLocalization.png "Changes in backoffice after changes in code")
### **6. Commit and Push**
1. Commit your changes to a new temporary branch (avoid committing directly to `contrib`).
2. Push the changes to your fork on GitHub.
### **7. Create a Pull Request**
1. In your forked repository on GitHub (`https://github.com/[YourUsername]/Umbraco-CMS`), a banner will appear stating that you pushed a new branch.
2. Click the button to create a pull request and follow the instructions.
## **I Cant Find the Correct Translation**
If you cant find the translation you need, it may not exist yet. In this case, you can create a new action with related keys.
### **1. Ensure It Doesnt Already Exist**
Search thoroughly in `en.ts` or `en-us.ts` for all relevant keywords.
### **2. Create an Action**
1. Choose a meaningful name for the action to avoid confusion. \
\
**Example:** Translation for the Data Type "Color Picker."
* **Good name:** `colorPickerConfigurations`
* **Bad name:** `colorpicker`
2. A specific action name prevents unnecessarily long key names.
3. Define the action:
### **3. Create Keys**
1. Use clear and descriptive key names. \
\
**Example:**
* **Good name:** `colorsTitle`
* **Bad name:** `colors`
2. Add the necessary keys inside the action with proper translations.
![Action and related Keys in language files](/.github/img/contributing/actionAndKeys.png "Action and related Keys in language files")
## **I Cant Find a <code>render()</code> Code in VS Code**
In some cases, such as Data Types, the label might not be inside `render()`. Instead, it may be in a manifest file.
### 1. Search for the Text
Copy the text from the Backoffice and search for it in the code.
### 2. Open the Manifest File
Once you find the relevant manifest file, open it to confirm youre in the right place.
### 3. Change the Label
In Markdown files, localization is slightly different. Instead of:
`${this.localize.term('action_key')}`
Use: `#action_key`
**Example:**
`#colorPickerConfigurations_showLabelTitle`
### 4. Change the Description
For descriptions in Markdown files, use:
`{umbLocalize: action_key}`
**Example:**
`{umbLocalize: colorPickerConfigurations_showLabelDescription}`
### 5. Save and Verify
Once all changes are made, your manifest should look something like this:
![Localization changes to manifest files](/.github/img/contributing/finishedManifestAfterLocalizatonChanges.png "Localization changes to manifest files")
---
### Thank you
Following these steps ensures that the Umbraco Backoffice remains accessible and user-friendly in all supported languages. Thanks for contributing! 🎉
Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 140 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 118 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 170 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 175 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 148 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

+7 -5
View File
@@ -3,8 +3,9 @@ name: Backoffice Static Web Apps CI/CD
on:
push:
branches:
- contrib
- main
- v*/dev
- v*/main
paths:
- src/Umbraco.Web.UI.Client/package.json
- src/Umbraco.Web.UI.Client/package-lock.json
@@ -13,12 +14,13 @@ on:
pull_request:
types: [opened, synchronize, reopened, closed]
branches:
- contrib
- main
- v*/dev
- v*/main
jobs:
build_and_deploy_job:
if: github.event_name == 'push' || (github.event_name == 'pull_request' && github.event.action != 'closed' && (contains(github.event.pull_request.labels.*.name, 'preview/backoffice')))
if: github.event_name == 'push' || (github.event_name == 'pull_request' && github.event.action != 'closed' && contains(github.event.pull_request.labels.*.name, 'preview/backoffice') && github.repository == github.event.pull_request.head.repo.full_name)
runs-on: ubuntu-latest
name: Build and Deploy Job
steps:
@@ -29,7 +31,7 @@ jobs:
id: builddeploy
uses: Azure/static-web-apps-deploy@v1
with:
production_branch: contrib
production_branch: main
azure_static_web_apps_api_token: ${{ secrets.AZURE_STATIC_WEB_APPS_API_TOKEN_VICTORIOUS_GROUND_017B08103 }}
repo_token: ${{ secrets.GITHUB_TOKEN }} # Used for Github integrations (i.e. PR comments)
action: "upload"
@@ -42,7 +44,7 @@ jobs:
###### End of Repository/Build Configurations ######
close_pull_request_job:
if: github.event_name == 'pull_request' && github.event.action == 'closed'
if: github.event_name == 'pull_request' && github.event.action == 'closed' && contains(github.event.pull_request.labels.*.name, 'preview/backoffice') && github.repository == github.event.pull_request.head.repo.full_name
runs-on: ubuntu-latest
name: Close Pull Request Job
steps:
+7 -5
View File
@@ -3,8 +3,9 @@ name: Storybook CI/CD
on:
push:
branches:
- contrib
- main
- v*/dev
- v*/main
paths:
- src/Umbraco.Web.UI.Client/package.json
- src/Umbraco.Web.UI.Client/package-lock.json
@@ -13,15 +14,16 @@ on:
pull_request:
types: [opened, synchronize, reopened, closed]
branches:
- contrib
- main
- v*/dev
- v*/main
env:
NODE_OPTIONS: --max_old_space_size=16384
jobs:
build_and_deploy_job:
if: github.event_name == 'push' || (github.event_name == 'pull_request' && github.event.action != 'closed' && (contains(github.event.pull_request.labels.*.name, 'preview/storybook')))
if: github.event_name == 'push' || (github.event_name == 'pull_request' && github.event.action != 'closed' && contains(github.event.pull_request.labels.*.name, 'preview/storybook') && github.repository == github.event.pull_request.head.repo.full_name)
runs-on: ubuntu-latest
name: Build and Deploy Job
steps:
@@ -30,7 +32,7 @@ jobs:
id: builddeploy
uses: Azure/static-web-apps-deploy@v1
with:
production_branch: contrib
production_branch: main
azure_static_web_apps_api_token: ${{ secrets.AZURE_STATIC_WEB_APPS_API_TOKEN_ORANGE_SEA_0C7411A03 }}
repo_token: ${{ secrets.GITHUB_TOKEN }} # Used for Github integrations (i.e. PR comments)
action: "upload"
@@ -43,7 +45,7 @@ jobs:
###### End of Repository/Build Configurations ######
close_pull_request_job:
if: github.event_name == 'pull_request' && github.event.action == 'closed'
if: github.event_name == 'pull_request' && github.event.action == 'closed' && contains(github.event.pull_request.labels.*.name, 'preview/storybook') && github.repository == github.event.pull_request.head.repo.full_name
runs-on: ubuntu-latest
name: Close Pull Request Job
steps:
+4 -4
View File
@@ -4,15 +4,15 @@ on:
push:
branches:
- "*/dev"
- "*/contrib"
- "contrib"
- "*/main"
- "main"
- "release/*"
pull_request:
# The branches below must be a subset of the branches above
branches:
- "*/dev"
- "*/contrib"
- "contrib"
- "*/main"
- "main"
- "release/*"
schedule:
- cron: "33 2 * * 1"
+4 -2
View File
@@ -3,16 +3,18 @@ name: Test Backoffice
on:
push:
branches:
- contrib
- main
- release/*
- v*/dev
- v*/main
paths:
- src/Umbraco.Web.UI.Client/**
pull_request:
branches:
- contrib
- main
- release/*
- v*/dev
- v*/main
paths:
- src/Umbraco.Web.UI.Client/**
+2
View File
@@ -62,6 +62,7 @@
"cwd": "${workspaceFolder}/src/Umbraco.Web.UI",
"stopAtEntry": false,
"requireExactSource": false,
"postDebugTask": "kill-umbraco-web-ui",
// Enable launching a web browser when ASP.NET Core starts. For more information: https://aka.ms/VSCode-CS-LaunchJson-WebBrowser
"serverReadyAction": {
"action": "openExternally",
@@ -96,6 +97,7 @@
"stopAtEntry": false,
"requireExactSource": false,
"checkForDevCert": true,
"postDebugTask": "kill-umbraco-web-ui",
"env": {
"ASPNETCORE_ENVIRONMENT": "Development",
"ASPNETCORE_URLS": "https://localhost:44339",
+85 -74
View File
@@ -1,76 +1,87 @@
{
"version": "2.0.0",
"tasks": [
{
"label": "Build",
"detail": "Builds the client and SLN",
"promptOnClose": true,
"group": "build",
"dependsOn": [
"Client Build",
"Dotnet build"
],
"problemMatcher": []
},
{
"label": "Client Install",
"detail": "install npm for Umbraco.Web.UI.Client",
"promptOnClose": true,
"type": "npm",
"script": "install",
"path": "src/Umbraco.Web.UI.Client/",
"problemMatcher": []
},
{
"label": "Client Build",
"detail": "runs npm run build for Umbraco.Web.UI.Client",
"promptOnClose": true,
"group": "build",
"type": "npm",
"script": "build:for:cms",
"path": "src/Umbraco.Web.UI.Client/",
"problemMatcher": []
},
{
"label": "Client Watch",
"detail": "runs npm run dev for Umbraco.Web.UI.Client",
"promptOnClose": true,
"group": "build",
"type": "npm",
"script": "dev",
"path": "src/Umbraco.Web.UI.Client/",
"problemMatcher": []
},
{
"label": "Dotnet build",
"detail": "Dotnet build of SLN",
"promptOnClose": true,
"group": "build",
"command": "dotnet",
"type": "process",
"args": [
"build",
"${workspaceFolder}/umbraco.sln",
"/property:GenerateFullPaths=true",
"/consoleloggerparameters:NoSummary"
],
"problemMatcher": "$msCompile"
},
{
"label": "Dotnet watch",
"detail": "Dotnet run and watch of Web.UI",
"promptOnClose": true,
"command": "dotnet",
"type": "process",
"args": [
"watch",
"run",
"--project",
"${workspaceFolder}/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj",
"/property:GenerateFullPaths=true",
"/consoleloggerparameters:NoSummary"
],
"problemMatcher": "$msCompile"
}
]
"version": "2.0.0",
"tasks": [
{
"label": "Build",
"detail": "Builds the client and SLN",
"promptOnClose": true,
"group": "build",
"dependsOn": ["Client Build", "Dotnet build"],
"problemMatcher": []
},
{
"label": "Client Install",
"detail": "install npm for Umbraco.Web.UI.Client",
"promptOnClose": true,
"type": "npm",
"script": "install",
"path": "src/Umbraco.Web.UI.Client/",
"problemMatcher": []
},
{
"label": "Client Build",
"detail": "runs npm run build for Umbraco.Web.UI.Client",
"promptOnClose": true,
"group": "build",
"type": "npm",
"script": "build:for:cms",
"path": "src/Umbraco.Web.UI.Client/",
"problemMatcher": []
},
{
"label": "Client Watch",
"detail": "runs npm run dev for Umbraco.Web.UI.Client",
"promptOnClose": true,
"group": "build",
"type": "npm",
"script": "dev",
"path": "src/Umbraco.Web.UI.Client/",
"problemMatcher": []
},
{
"label": "Dotnet build",
"detail": "Dotnet build of SLN",
"promptOnClose": true,
"group": "build",
"command": "dotnet",
"type": "process",
"args": [
"build",
"${workspaceFolder}/umbraco.sln",
"/property:GenerateFullPaths=true",
"/consoleloggerparameters:NoSummary"
],
"problemMatcher": "$msCompile"
},
{
"label": "Dotnet watch",
"detail": "Dotnet run and watch of Web.UI",
"promptOnClose": true,
"command": "dotnet",
"type": "process",
"args": [
"watch",
"run",
"--project",
"${workspaceFolder}/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj",
"/property:GenerateFullPaths=true",
"/consoleloggerparameters:NoSummary"
],
"problemMatcher": "$msCompile"
},
{
"label": "kill-umbraco-web-ui",
"type": "shell",
"problemMatcher": [],
"osx": {
"command": "pkill -f Umbraco.Web.UI"
},
"linux": {
"command": "pkill -f Umbraco.Web.UI"
},
"windows": {
"command": "taskkill /IM Umbraco.Web.UI.exe /F"
}
}
]
}
+1 -1
View File
@@ -40,7 +40,7 @@
<!-- Package Validation -->
<PropertyGroup>
<GenerateCompatibilitySuppressionFile>false</GenerateCompatibilitySuppressionFile>
<EnablePackageValidation>false</EnablePackageValidation> <!-- TODO: Update to true once 16.0.0 is released to NuGet. -->
<EnablePackageValidation>true</EnablePackageValidation>
<PackageValidationBaselineVersion>16.0.0</PackageValidationBaselineVersion>
<EnableStrictModeForCompatibleFrameworksInPackage>true</EnableStrictModeForCompatibleFrameworksInPackage>
<EnableStrictModeForCompatibleTfms>true</EnableStrictModeForCompatibleTfms>
+20 -2
View File
@@ -567,7 +567,8 @@ stages:
"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" | Out-File .env
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
@@ -650,6 +651,14 @@ stages:
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
@@ -725,7 +734,8 @@ stages:
"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" | Out-File .env
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
@@ -826,6 +836,14 @@ stages:
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
+41 -15
View File
@@ -8,10 +8,17 @@ schedules:
displayName: Daily midnight build
branches:
include:
- v14/dev
- v15/dev
- release/16.0
- main
parameters:
# Skipped due to DB locks
- name: sqliteAcceptanceTests
displayName: Run SQLite Acceptance Tests
type: boolean
default: false
variables:
nodeVersion: 20
solution: umbraco.sln
@@ -122,6 +129,7 @@ stages:
# E2E Tests
- job:
displayName: E2E Tests (SQLite)
condition: eq(${{parameters.sqliteAcceptanceTests}}, True)
timeoutInMinutes: 180
variables:
# Connection string
@@ -131,22 +139,22 @@ stages:
matrix:
LinuxPart1Of3:
vmImage: "ubuntu-latest"
testCommand: "npm run test -- --shard=1/3"
testCommand: "npm run testSqlite -- --shard=1/3"
LinuxPart2Of3:
vmImage: "ubuntu-latest"
testCommand: "npm run test -- --shard=2/3"
testCommand: "npm run testSqlite -- --shard=2/3"
LinuxPart3Of3:
vmImage: "ubuntu-latest"
testCommand: "npm run test -- --shard=3/3"
testCommand: "npm run testSqlite -- --shard=3/3"
WindowsPart1Of3:
vmImage: "windows-latest"
testCommand: "npm run test -- --shard=1/3"
testCommand: "npm run testSqlite -- --shard=1/3"
WindowsPart2Of3:
vmImage: "windows-latest"
testCommand: "npm run test -- --shard=2/3"
testCommand: "npm run testSqlite -- --shard=2/3"
WindowsPart3Of3:
vmImage: "windows-latest"
testCommand: "npm run test -- --shard=3/3"
testCommand: "npm run testSqlite -- --shard=3/3"
pool:
vmImage: $(vmImage)
steps:
@@ -172,7 +180,8 @@ stages:
"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" | Out-File .env
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
@@ -260,6 +269,14 @@ stages:
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
- task: PublishPipelineArtifact@1
displayName: Publish test artifacts
@@ -288,29 +305,29 @@ stages:
strategy:
matrix:
LinuxPart1Of3:
testCommand: "npm run testSqlite -- --shard=1/3"
testCommand: "npm run test -- --shard=1/3"
vmImage: "ubuntu-latest"
SA_PASSWORD: $(UMBRACO__CMS__UNATTENDED__UNATTENDEDUSERPASSWORD)
CONNECTIONSTRINGS__UMBRACODBDSN: "Server=(local);Database=Umbraco;User Id=sa;Password=$(SA_PASSWORD);Encrypt=True;TrustServerCertificate=True"
LinuxPart2Of3:
testCommand: "npm run testSqlite -- --shard=2/3"
testCommand: "npm run test -- --shard=2/3"
vmImage: "ubuntu-latest"
SA_PASSWORD: $(UMBRACO__CMS__UNATTENDED__UNATTENDEDUSERPASSWORD)
CONNECTIONSTRINGS__UMBRACODBDSN: "Server=(local);Database=Umbraco;User Id=sa;Password=$(SA_PASSWORD);Encrypt=True;TrustServerCertificate=True"
LinuxPart3Of3:
testCommand: "npm run testSqlite -- --shard=3/3"
testCommand: "npm run test -- --shard=3/3"
vmImage: "ubuntu-latest"
SA_PASSWORD: $(UMBRACO__CMS__UNATTENDED__UNATTENDEDUSERPASSWORD)
CONNECTIONSTRINGS__UMBRACODBDSN: "Server=(local);Database=Umbraco;User Id=sa;Password=$(SA_PASSWORD);Encrypt=True;TrustServerCertificate=True"
WindowsPart1Of3:
vmImage: "windows-latest"
testCommand: "npm run testSqlite -- --shard=1/3"
testCommand: "npm run test -- --shard=1/3"
WindowsPart2Of3:
vmImage: "windows-latest"
testCommand: "npm run testSqlite -- --shard=2/3"
testCommand: "npm run test -- --shard=2/3"
WindowsPart3Of3:
vmImage: "windows-latest"
testCommand: "npm run testSqlite -- --shard=3/3"
testCommand: "npm run test -- --shard=3/3"
pool:
vmImage: $(vmImage)
steps:
@@ -335,7 +352,8 @@ stages:
"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" | Out-File .env
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
@@ -441,6 +459,14 @@ stages:
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
- task: PublishPipelineArtifact@1
displayName: Publish test artifacts
+1 -3
View File
@@ -10,9 +10,7 @@ schedules:
include:
- v10/dev
- v13/dev
- v14/dev
- v15/dev
- v16/dev
- main
steps:
- checkout: none
@@ -18,7 +18,7 @@ public class ProcessRequestContextHandler
var backOfficePathSegment = Constants.System.DefaultUmbracoPath.TrimStart(Constants.CharArrays.Tilde)
.EnsureStartsWith('/')
.EnsureEndsWith('/');
_pathsToHandle = [backOfficePathSegment, "/.well-known/openid-configuration"];
_pathsToHandle = [backOfficePathSegment, "/.well-known/openid-configuration", "/.well-known/jwks"];
}
public ValueTask HandleAsync(OpenIddictServerEvents.ProcessRequestContext context)
@@ -1,5 +1,6 @@
using Microsoft.AspNetCore.OutputCaching;
using Microsoft.AspNetCore.OutputCaching;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Primitives;
using Umbraco.Cms.Core.DeliveryApi;
namespace Umbraco.Cms.Api.Delivery.Caching;
@@ -7,9 +8,13 @@ namespace Umbraco.Cms.Api.Delivery.Caching;
internal sealed class DeliveryApiOutputCachePolicy : IOutputCachePolicy
{
private readonly TimeSpan _duration;
private readonly StringValues _varyByHeaderNames;
public DeliveryApiOutputCachePolicy(TimeSpan duration)
=> _duration = duration;
public DeliveryApiOutputCachePolicy(TimeSpan duration, StringValues varyByHeaderNames)
{
_duration = duration;
_varyByHeaderNames = varyByHeaderNames;
}
ValueTask IOutputCachePolicy.CacheRequestAsync(OutputCacheContext context, CancellationToken cancellationToken)
{
@@ -20,6 +25,7 @@ internal sealed class DeliveryApiOutputCachePolicy : IOutputCachePolicy
context.EnableOutputCaching = requestPreviewService.IsPreview() is false;
context.ResponseExpirationTimeSpan = _duration;
context.CacheVaryByRules.HeaderNames = _varyByHeaderNames;
return ValueTask.CompletedTask;
}
@@ -5,6 +5,7 @@ using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Primitives;
using Umbraco.Cms.Api.Common.DependencyInjection;
using Umbraco.Cms.Api.Delivery.Accessors;
using Umbraco.Cms.Api.Delivery.Caching;
@@ -110,12 +111,20 @@ public static class UmbracoBuilderExtensions
if (outputCacheSettings.ContentDuration.TotalSeconds > 0)
{
options.AddPolicy(Constants.DeliveryApi.OutputCache.ContentCachePolicy, new DeliveryApiOutputCachePolicy(outputCacheSettings.ContentDuration));
options.AddPolicy(
Constants.DeliveryApi.OutputCache.ContentCachePolicy,
new DeliveryApiOutputCachePolicy(
outputCacheSettings.ContentDuration,
new StringValues([Constants.DeliveryApi.HeaderNames.AcceptLanguage, Constants.DeliveryApi.HeaderNames.AcceptSegment, Constants.DeliveryApi.HeaderNames.StartItem])));
}
if (outputCacheSettings.MediaDuration.TotalSeconds > 0)
{
options.AddPolicy(Constants.DeliveryApi.OutputCache.MediaCachePolicy, new DeliveryApiOutputCachePolicy(outputCacheSettings.MediaDuration));
options.AddPolicy(
Constants.DeliveryApi.OutputCache.MediaCachePolicy,
new DeliveryApiOutputCachePolicy(
outputCacheSettings.MediaDuration,
Constants.DeliveryApi.HeaderNames.StartItem));
}
});
@@ -1,4 +1,4 @@
using Microsoft.OpenApi.Any;
using Microsoft.OpenApi.Any;
using Microsoft.OpenApi.Models;
using Swashbuckle.AspNetCore.SwaggerGen;
using Umbraco.Cms.Api.Delivery.Configuration;
@@ -21,7 +21,7 @@ internal sealed class SwaggerContentDocumentationFilter : SwaggerDocumentationFi
operation.Parameters.Add(new OpenApiParameter
{
Name = "Accept-Language",
Name = Core.Constants.DeliveryApi.HeaderNames.AcceptLanguage,
In = ParameterLocation.Header,
Required = false,
Description = "Defines the language to return. Use this when querying language variant content items.",
@@ -35,7 +35,7 @@ internal sealed class SwaggerContentDocumentationFilter : SwaggerDocumentationFi
operation.Parameters.Add(new OpenApiParameter
{
Name = "Accept-Segment",
Name = Core.Constants.DeliveryApi.HeaderNames.AcceptSegment,
In = ParameterLocation.Header,
Required = false,
Description = "Defines the segment to return. Use this when querying segment variant content items.",
@@ -51,7 +51,7 @@ internal sealed class SwaggerContentDocumentationFilter : SwaggerDocumentationFi
operation.Parameters.Add(new OpenApiParameter
{
Name = "Preview",
Name = Core.Constants.DeliveryApi.HeaderNames.Preview,
In = ParameterLocation.Header,
Required = false,
Description = "Whether to request draft content.",
@@ -60,7 +60,7 @@ internal sealed class SwaggerContentDocumentationFilter : SwaggerDocumentationFi
operation.Parameters.Add(new OpenApiParameter
{
Name = "Start-Item",
Name = Core.Constants.DeliveryApi.HeaderNames.StartItem,
In = ParameterLocation.Header,
Required = false,
Description = "URL segment or GUID of a root content item.",
@@ -1,4 +1,4 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc;
using Microsoft.OpenApi.Any;
using Microsoft.OpenApi.Models;
using Swashbuckle.AspNetCore.SwaggerGen;
@@ -63,7 +63,7 @@ internal abstract class SwaggerDocumentationFilterBase<TBaseController>
protected void AddApiKey(OpenApiOperation operation) =>
operation.Parameters.Add(new OpenApiParameter
{
Name = "Api-Key",
Name = Core.Constants.DeliveryApi.HeaderNames.ApiKey,
In = ParameterLocation.Header,
Required = false,
Description = "API key specified through configuration to authorize access to the API.",
@@ -1,4 +1,4 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Options;
using Umbraco.Cms.Core.Configuration.Models;
using Umbraco.Cms.Core.DeliveryApi;
@@ -29,7 +29,7 @@ internal sealed class ApiAccessService : RequestHeaderHandler, IApiAccessService
private bool IfEnabled(Func<bool> condition) => _deliveryApiSettings.Enabled && condition();
private bool HasValidApiKey() => _deliveryApiSettings.ApiKey.IsNullOrWhiteSpace() == false
&& _deliveryApiSettings.ApiKey.Equals(GetHeaderValue("Api-Key"));
&& _deliveryApiSettings.ApiKey.Equals(GetHeaderValue(Core.Constants.DeliveryApi.HeaderNames.ApiKey));
private bool IfMediaEnabled(Func<bool> condition) => _deliveryApiSettings is { Enabled: true, Media.Enabled: true } && condition();
}
@@ -11,5 +11,5 @@ internal sealed class RequestPreviewService : RequestHeaderHandler, IRequestPrev
}
/// <inheritdoc />
public bool IsPreview() => string.Equals(GetHeaderValue("Preview"), "true", StringComparison.OrdinalIgnoreCase);
public bool IsPreview() => string.Equals(GetHeaderValue(Core.Constants.DeliveryApi.HeaderNames.Preview), "true", StringComparison.OrdinalIgnoreCase);
}
@@ -1,4 +1,4 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http;
using Umbraco.Cms.Core.DeliveryApi;
namespace Umbraco.Cms.Api.Delivery.Services;
@@ -12,5 +12,5 @@ internal sealed class RequestSegmentService : RequestHeaderHandler, IRequestSegm
/// <inheritdoc />
public string? GetRequestedSegment()
=> GetHeaderValue("Accept-Segment");
=> GetHeaderValue(Core.Constants.DeliveryApi.HeaderNames.AcceptSegment);
}
@@ -60,5 +60,5 @@ internal sealed class RequestStartItemProvider : RequestHeaderHandler, IRequestS
}
/// <inheritdoc/>
public string? RequestedStartItem() => GetHeaderValue("Start-Item");
public string? RequestedStartItem() => GetHeaderValue(Constants.DeliveryApi.HeaderNames.StartItem);
}
@@ -36,7 +36,7 @@ internal abstract class RoutingServiceBase
}
protected static string GetContentRoute(DomainAndUri domainAndUri, Uri contentRoute)
=> $"{domainAndUri.ContentId}{DomainUtilities.PathRelativeToDomain(domainAndUri.Uri, contentRoute.AbsolutePath)}";
=> $"{domainAndUri.ContentId}{DomainUtilities.PathRelativeToDomain(domainAndUri.Uri, contentRoute.LocalPath)}"; // Use LocalPath over AbsolutePath to keep the path decoded.
protected DomainAndUri? GetDomainAndUriForRoute(Uri contentUrl)
{
@@ -22,6 +22,9 @@
<AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleTo">
<_Parameter1>Umbraco.Tests.UnitTests</_Parameter1>
</AssemblyAttribute>
<AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleTo">
<_Parameter1>Umbraco.Tests.Integration</_Parameter1>
</AssemblyAttribute>
<AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleTo">
<_Parameter1>DynamicProxyGenAssembly2</_Parameter1>
</AssemblyAttribute>
@@ -0,0 +1,19 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Api.Management.ViewModels.Tree;
using Umbraco.Cms.Core.Services;
namespace Umbraco.Cms.Api.Management.Controllers.DataType.Tree;
public class SiblingsDataTypeTreeController : DataTypeTreeControllerBase
{
public SiblingsDataTypeTreeController(IEntityService entityService, IDataTypeService dataTypeService)
: base(entityService, dataTypeService)
{
}
[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);
}
@@ -53,16 +53,6 @@ public class MoveDocumentController : DocumentControllerBase
return Forbidden();
}
AuthorizationResult authorizationResult = await _authorizationService.AuthorizeResourceAsync(
User,
ContentPermissionResource.WithKeys(ActionMove.ActionLetter, new[] { moveDocumentRequestModel.Target?.Id, id }),
AuthorizationPolicies.ContentPermissionByResource);
if (!authorizationResult.Succeeded)
{
return Forbidden();
}
Attempt<IContent?, ContentEditingOperationStatus> result = await _contentEditingService.MoveAsync(
id,
moveDocumentRequestModel.Target?.Id,
@@ -0,0 +1,40 @@
using Asp.Versioning;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Api.Management.Factories;
using Umbraco.Cms.Api.Management.Services.Entities;
using Umbraco.Cms.Api.Management.ViewModels.Tree;
using Umbraco.Cms.Core.Cache;
using Umbraco.Cms.Core.Security;
using Umbraco.Cms.Core.Services;
namespace Umbraco.Cms.Api.Management.Controllers.Document.Tree;
[ApiVersion("1.0")]
public class SiblingsDocumentTreeController : DocumentTreeControllerBase
{
public SiblingsDocumentTreeController(
IEntityService entityService,
IUserStartNodeEntitiesService userStartNodeEntitiesService,
IDataTypeService dataTypeService,
IPublicAccessService publicAccessService,
AppCaches appCaches,
IBackOfficeSecurityAccessor backofficeSecurityAccessor,
IDocumentPresentationFactory documentPresentationFactory)
: base(
entityService,
userStartNodeEntitiesService,
dataTypeService,
publicAccessService,
appCaches,
backofficeSecurityAccessor,
documentPresentationFactory)
{
}
[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);
}
@@ -0,0 +1,24 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Api.Management.Factories;
using Umbraco.Cms.Api.Management.ViewModels.Tree;
using Umbraco.Cms.Core.Services;
namespace Umbraco.Cms.Api.Management.Controllers.DocumentBlueprint.Tree;
public class SiblingsDocumentBlueprintTreeController : DocumentBlueprintTreeControllerBase
{
public SiblingsDocumentBlueprintTreeController(IEntityService entityService, IDocumentPresentationFactory documentPresentationFactory)
: base(entityService, documentPresentationFactory)
{
}
[HttpGet("siblings")]
[ProducesResponseType(typeof(IEnumerable<DocumentBlueprintTreeItemResponseModel>), StatusCodes.Status200OK)]
public Task<ActionResult<IEnumerable<DocumentBlueprintTreeItemResponseModel>>> Siblings(
CancellationToken cancellationToken,
Guid target,
int before,
int after) =>
GetSiblings(target, before, after);
}
@@ -0,0 +1,23 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Api.Management.ViewModels.Tree;
using Umbraco.Cms.Core.Services;
namespace Umbraco.Cms.Api.Management.Controllers.DocumentType.Tree;
public class SiblingsDocumentTypeTreeController : DocumentTypeTreeControllerBase
{
public SiblingsDocumentTypeTreeController(IEntityService entityService, IContentTypeService contentTypeService)
: base(entityService, contentTypeService)
{
}
[HttpGet("siblings")]
[ProducesResponseType(typeof(IEnumerable<DocumentTypeTreeItemResponseModel>), StatusCodes.Status200OK)]
public Task<ActionResult<IEnumerable<DocumentTypeTreeItemResponseModel>>> Siblings(
CancellationToken cancellationToken,
Guid target,
int before,
int after) =>
GetSiblings(target, before, after);
}
@@ -13,6 +13,7 @@ using Umbraco.Cms.Core.Features;
using Umbraco.Cms.Core.Models.Membership;
using Umbraco.Cms.Core.Security;
using Umbraco.Cms.Web.Common.Authorization;
using Umbraco.Cms.Web.Common.Filters;
namespace Umbraco.Cms.Api.Management.Controllers;
@@ -22,6 +23,7 @@ namespace Umbraco.Cms.Api.Management.Controllers;
[MapToApi(ManagementApiConfiguration.ApiName)]
[JsonOptionsName(Constants.JsonOptionsNames.BackOffice)]
[AppendEventMessages]
[DisableBrowserCache]
[Produces("application/json")]
public abstract class ManagementApiControllerBase : Controller, IUmbracoFeature
{
@@ -0,0 +1,29 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Api.Management.Factories;
using Umbraco.Cms.Api.Management.Services.Entities;
using Umbraco.Cms.Api.Management.ViewModels.Tree;
using Umbraco.Cms.Core.Cache;
using Umbraco.Cms.Core.Security;
using Umbraco.Cms.Core.Services;
namespace Umbraco.Cms.Api.Management.Controllers.Media.Tree;
public class SiblingsMediaTreeController : MediaTreeControllerBase
{
public SiblingsMediaTreeController(
IEntityService entityService,
IUserStartNodeEntitiesService userStartNodeEntitiesService,
IDataTypeService dataTypeService,
AppCaches appCaches,
IBackOfficeSecurityAccessor backofficeSecurityAccessor,
IMediaPresentationFactory mediaPresentationFactory)
: base(entityService, userStartNodeEntitiesService, dataTypeService, appCaches, backofficeSecurityAccessor, mediaPresentationFactory)
{
}
[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);
}
@@ -0,0 +1,19 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Api.Management.ViewModels.Tree;
using Umbraco.Cms.Core.Services;
namespace Umbraco.Cms.Api.Management.Controllers.MediaType.Tree;
public class SiblingsMediaTypeTreeController : MediaTypeTreeControllerBase
{
public SiblingsMediaTypeTreeController(IEntityService entityService, IMediaTypeService mediaTypeService)
: base(entityService, mediaTypeService)
{
}
[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);
}
@@ -1,8 +1,11 @@
using Asp.Versioning;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.DependencyInjection;
using Umbraco.Cms.Api.Common.ViewModels.Pagination;
using Umbraco.Cms.Api.Management.Factories;
using Umbraco.Cms.Api.Management.ViewModels.Package;
using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Core.Mapping;
using Umbraco.Cms.Core.Packaging;
using Umbraco.Cms.Core.Services;
@@ -14,12 +17,25 @@ namespace Umbraco.Cms.Api.Management.Controllers.Package;
public class AllMigrationStatusPackageController : PackageControllerBase
{
private readonly IPackagingService _packagingService;
private readonly IUmbracoMapper _umbracoMapper;
private readonly IPackagePresentationFactory _packagePresentationFactory;
[Obsolete("Please use the non-obsolete constructor. Scheduled for removal in V18.")]
public AllMigrationStatusPackageController(IPackagingService packagingService, IUmbracoMapper umbracoMapper)
: this(packagingService, StaticServiceProvider.Instance.GetRequiredService<IPackagePresentationFactory>())
{
}
[Obsolete("Please use the non-obsolete constructor. Scheduled for removal in V18.")]
public AllMigrationStatusPackageController(IPackagingService packagingService, IUmbracoMapper umbracoMapper, IPackagePresentationFactory packagePresentationFactory)
: this(packagingService, packagePresentationFactory)
{
}
[ActivatorUtilitiesConstructor]
public AllMigrationStatusPackageController(IPackagingService packagingService, IPackagePresentationFactory packagePresentationFactory)
{
_packagingService = packagingService;
_umbracoMapper = umbracoMapper;
_packagePresentationFactory = packagePresentationFactory;
}
/// <summary>
@@ -38,11 +54,7 @@ public class AllMigrationStatusPackageController : PackageControllerBase
{
PagedModel<InstalledPackage> migrationPlans = await _packagingService.GetInstalledPackagesFromMigrationPlansAsync(skip, take);
var viewModel = new PagedViewModel<PackageMigrationStatusResponseModel>
{
Total = migrationPlans.Total,
Items = _umbracoMapper.MapEnumerable<InstalledPackage, PackageMigrationStatusResponseModel>(migrationPlans.Items)
};
PagedViewModel<PackageMigrationStatusResponseModel> viewModel = _packagePresentationFactory.CreatePackageMigrationStatusResponseModel(migrationPlans);
return Ok(viewModel);
}
@@ -15,7 +15,7 @@ public class RebuildPublishedCacheController : PublishedCacheControllerBase
[HttpPost("rebuild")]
[MapToApiVersion("1.0")]
[ProducesResponseType(StatusCodes.Status200OK)]
public async Task<IActionResult> Rebuild(CancellationToken cancellationToken)
public Task<IActionResult> Rebuild(CancellationToken cancellationToken)
{
if (_databaseCacheRebuilder.IsRebuilding())
{
@@ -27,10 +27,10 @@ public class RebuildPublishedCacheController : PublishedCacheControllerBase
Type = "Error",
};
return await Task.FromResult(Conflict(problemDetails));
return Task.FromResult<IActionResult>(Conflict(problemDetails));
}
_databaseCacheRebuilder.Rebuild(true);
return await Task.FromResult(Ok());
return Task.FromResult<IActionResult>(Ok());
}
}
@@ -1,5 +1,4 @@
using Asp.Versioning;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Api.Common.ViewModels.Pagination;
@@ -9,7 +8,6 @@ using Umbraco.Cms.Core.Mapping;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Services.OperationStatus;
using Umbraco.Cms.Web.Common.Authorization;
namespace Umbraco.Cms.Api.Management.Controllers.Segment;
@@ -29,9 +27,9 @@ public class AllSegmentController : SegmentControllerBase
[MapToApiVersion("1.0")]
[ProducesResponseType(typeof(PagedViewModel<SegmentResponseModel>), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)]
public async Task<IActionResult> GetAll(CancellationToken cancellationToken, int skip = 0, int take = 100)
public async Task<IActionResult> GetAll(CancellationToken cancellationToken, int skip = 0, int take = 100, Guid? documentId = null, Guid? documentTypeId = null)
{
Attempt<PagedModel<Core.Models.Segment>?, SegmentOperationStatus> pagedAttempt = await _segmentService.GetPagedSegmentsAsync(skip, take);
Attempt<PagedModel<Core.Models.Segment>?, SegmentOperationStatus> pagedAttempt = await _segmentService.GetPagedSegmentsAsync(skip, take, documentId, documentTypeId);
if (pagedAttempt.Success is false)
{
@@ -0,0 +1,23 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Api.Management.ViewModels.Tree;
using Umbraco.Cms.Core.Services;
namespace Umbraco.Cms.Api.Management.Controllers.Template.Tree;
public class SiblingsTemplateTreeController : TemplateTreeControllerBase
{
public SiblingsTemplateTreeController(IEntityService entityService)
: base(entityService)
{
}
[HttpGet("siblings")]
[ProducesResponseType(typeof(IEnumerable<NamedEntityTreeItemResponseModel>), StatusCodes.Status200OK)]
public Task<ActionResult<IEnumerable<NamedEntityTreeItemResponseModel>>> Siblings(
CancellationToken cancellationToken,
Guid target,
int before,
int after) =>
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,6 +44,23 @@ 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)
{
IEntitySlim[] siblings = EntityService.GetSiblings(target, ItemObjectType, before, after, ItemOrdering).ToArray();
if (siblings.Length == 0)
{
return Task.FromResult<ActionResult<IEnumerable<TItem>>>(NotFound());
}
IEntitySlim? entity = siblings.FirstOrDefault();
Guid? parentKey = entity?.ParentId > 0
? EntityService.GetKey(entity.ParentId, ItemObjectType).Result
: Constants.System.RootKey;
TItem[] treeItemsViewModels = MapTreeItemViewModels(parentKey, siblings);
return Task.FromResult<ActionResult<IEnumerable<TItem>>>(Ok(treeItemsViewModels));
}
protected virtual async Task<ActionResult<IEnumerable<TItem>>> GetAncestors(Guid descendantKey, bool includeSelf = true)
{
IEntitySlim[] ancestorEntities = await GetAncestorEntitiesAsync(descendantKey, includeSelf);
@@ -43,7 +43,7 @@ internal static class ApplicationBuilderExtensions
var response = new ProblemDetails
{
Title = exception.Message,
Title = isDebug ? exception.Message : "Server Error",
Detail = isDebug ? exception.StackTrace : null,
Status = statusCode ?? StatusCodes.Status500InternalServerError,
Instance = isDebug ? exception.GetType().Name : null,
@@ -1,5 +1,6 @@
using Umbraco.Cms.Api.Management.ViewModels;
using Umbraco.Cms.Api.Management.ViewModels.AuditLog;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.Membership;
using Umbraco.Cms.Core.Services;
@@ -19,19 +20,17 @@ public class AuditLogPresentationFactory : IAuditLogPresentationFactory
public IEnumerable<AuditLogResponseModel> CreateAuditLogViewModel(IEnumerable<IAuditItem> auditItems) => auditItems.Select(CreateAuditLogViewModel);
private AuditLogResponseModel CreateAuditLogViewModel(IAuditItem auditItem)
{
Guid userKey = _userIdKeyResolver.GetAsync(auditItem.UserId).GetAwaiter().GetResult();
IUser user = _userService.GetAsync(userKey).GetAwaiter().GetResult()
?? throw new ArgumentException($"Could not find user with id {auditItem.UserId}");
return new AuditLogResponseModel
private AuditLogResponseModel CreateAuditLogViewModel(IAuditItem auditItem) =>
new()
{
Comment = auditItem.Comment,
LogType = auditItem.AuditType,
Parameters = auditItem.Parameters,
Timestamp = auditItem.CreateDate,
User = new ReferenceByIdModel(user.Key)
User = auditItem.UserId switch
{
Constants.Security.UnknownUserId => new ReferenceByIdModel(),
_ => new ReferenceByIdModel(_userIdKeyResolver.GetAsync(auditItem.UserId).GetAwaiter().GetResult()),
},
};
}
}
@@ -1,5 +1,6 @@
using Umbraco.Cms.Api.Management.ViewModels;
using Umbraco.Cms.Api.Management.ViewModels.Document;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Services;
using Umbraco.Extensions;
@@ -19,17 +20,26 @@ internal sealed class DocumentVersionPresentationFactory : IDocumentVersionPrese
_userIdKeyResolver = userIdKeyResolver;
}
public async Task<DocumentVersionItemResponseModel> CreateAsync(ContentVersionMeta contentVersion) =>
new(
public async Task<DocumentVersionItemResponseModel> CreateAsync(ContentVersionMeta contentVersion)
{
ReferenceByIdModel userReference = contentVersion.UserId switch
{
Constants.Security.UnknownUserId => new ReferenceByIdModel(),
_ => new ReferenceByIdModel(await _userIdKeyResolver.GetAsync(contentVersion.UserId)),
};
return new DocumentVersionItemResponseModel(
contentVersion.VersionId.ToGuid(), // this is a magic guid since versions do not have keys in the DB
new ReferenceByIdModel(_entityService.GetKey(contentVersion.ContentId, UmbracoObjectTypes.Document).Result),
new ReferenceByIdModel(_entityService.GetKey(contentVersion.ContentTypeId, UmbracoObjectTypes.DocumentType)
.Result),
new ReferenceByIdModel(await _userIdKeyResolver.GetAsync(contentVersion.UserId)),
new ReferenceByIdModel(
_entityService.GetKey(contentVersion.ContentTypeId, UmbracoObjectTypes.DocumentType)
.Result),
userReference,
new DateTimeOffset(contentVersion.VersionDate),
contentVersion.CurrentPublishedVersion,
contentVersion.CurrentDraftVersion,
contentVersion.PreventCleanup);
}
public async Task<IEnumerable<DocumentVersionItemResponseModel>> CreateMultipleAsync(IEnumerable<ContentVersionMeta> contentVersions)
=> await CreateMultipleImplAsync(contentVersions).ToArrayAsync();
@@ -1,4 +1,6 @@
using Umbraco.Cms.Api.Common.ViewModels.Pagination;
using Umbraco.Cms.Api.Management.ViewModels.Package;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Packaging;
namespace Umbraco.Cms.Api.Management.Factories;
@@ -8,4 +10,6 @@ public interface IPackagePresentationFactory
PackageDefinition CreatePackageDefinition(CreatePackageRequestModel createPackageRequestModel);
PackageConfigurationResponseModel CreateConfigurationResponseModel();
PagedViewModel<PackageMigrationStatusResponseModel> CreatePackageMigrationStatusResponseModel(PagedModel<InstalledPackage> installedPackages) => new();
}
@@ -6,25 +6,58 @@ using Umbraco.Cms.Core.Models.Membership;
namespace Umbraco.Cms.Api.Management.Factories;
/// <summary>
/// Defines factory methods for the creation of user presentation models.
/// </summary>
public interface IUserPresentationFactory
{
/// <summary>
/// Creates a response model for the provided user.
/// </summary>
UserResponseModel CreateResponseModel(IUser user);
/// <summary>
/// Creates a create model for a user based on the provided request model.
/// </summary>
Task<UserCreateModel> CreateCreationModelAsync(CreateUserRequestModel requestModel);
/// <summary>
/// Creates an invite model for a user based on the provided request model.
/// </summary>
Task<UserInviteModel> CreateInviteModelAsync(InviteUserRequestModel requestModel);
/// <summary>
/// Creates an update model for an existing user based on the provided request model.
/// </summary>
Task<UserUpdateModel> CreateUpdateModelAsync(Guid existingUserKey, UpdateUserRequestModel updateModel);
/// <summary>
/// Creates a response model for the current user based on the provided user.
/// </summary>
Task<CurrentUserResponseModel> CreateCurrentUserResponseModelAsync(IUser user);
/// <summary>
/// Creates an resend invite model for a user based on the provided request model.
/// </summary>
Task<UserResendInviteModel> CreateResendInviteModelAsync(ResendInviteUserRequestModel requestModel);
/// <summary>
/// Creates a user configuration model that contains the necessary data for user management operations.
/// </summary>
Task<UserConfigurationResponseModel> CreateUserConfigurationModelAsync();
/// <summary>
/// Creates a current user configuration model that contains the necessary data for the current user's management operations.
/// </summary>
Task<CurrentUserConfigurationResponseModel> CreateCurrentUserConfigurationModelAsync();
/// <summary>
/// Creates a user item response model for the provided user.
/// </summary>
UserItemResponseModel CreateItemResponseModel(IUser user);
/// <summary>
/// Creates a calculated user start nodes response model based on the provided user.
/// </summary>
Task<CalculatedUserStartNodesResponseModel> CreateCalculatedUserStartNodesResponseModelAsync(IUser user);
}
@@ -1,10 +1,12 @@
using System.Collections.Specialized;
using System.Web;
using Microsoft.Extensions.Options;
using Umbraco.Cms.Api.Common.ViewModels.Pagination;
using Umbraco.Cms.Api.Management.ViewModels.Package;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Configuration.Models;
using Umbraco.Cms.Core.Mapping;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Packaging;
using Umbraco.Cms.Core.Services;
using Umbraco.Extensions;
@@ -42,6 +44,25 @@ internal class PackagePresentationFactory : IPackagePresentationFactory
MarketplaceUrl = GetMarketplaceUrl(),
};
public PagedViewModel<PackageMigrationStatusResponseModel> CreatePackageMigrationStatusResponseModel(PagedModel<InstalledPackage> installedPackages)
{
InstalledPackage[] installedPackagesAsArray = installedPackages.Items as InstalledPackage[] ?? installedPackages.Items.ToArray();
return new PagedViewModel<PackageMigrationStatusResponseModel>
{
Total = installedPackages.Total,
Items = installedPackagesAsArray
.GroupBy(package => package.PackageName)
.Select(packages => packages.OrderByDescending(package => package.HasPendingMigrations).First())
.Select(package => new PackageMigrationStatusResponseModel
{
PackageName = package.PackageName ?? string.Empty,
HasPendingMigrations = package.HasPendingMigrations,
})
.ToArray(),
};
}
private string GetMarketplaceUrl()
{
var uriBuilder = new UriBuilder(Constants.Marketplace.Url);
@@ -1,5 +1,6 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using Umbraco.Cms.Api.Management.Mapping.Permissions;
using Umbraco.Cms.Api.Management.Routing;
using Umbraco.Cms.Api.Management.Security;
using Umbraco.Cms.Api.Management.ViewModels;
@@ -22,6 +23,9 @@ using Umbraco.Extensions;
namespace Umbraco.Cms.Api.Management.Factories;
/// <summary>
/// Factory for creating user presentation models, implementing <see cref="IUserPresentationFactory"/>.
/// </summary>
public class UserPresentationFactory : IUserPresentationFactory
{
private readonly IEntityService _entityService;
@@ -34,9 +38,11 @@ public class UserPresentationFactory : IUserPresentationFactory
private readonly IPasswordConfigurationPresentationFactory _passwordConfigurationPresentationFactory;
private readonly IBackOfficeExternalLoginProviders _externalLoginProviders;
private readonly SecuritySettings _securitySettings;
private readonly IUserService _userService;
private readonly IContentService _contentService;
private readonly Dictionary<Type, IPermissionPresentationMapper> _permissionPresentationMappersByType;
/// <summary>
/// Initializes a new instance of the <see cref="UserPresentationFactory"/> class.
/// </summary>
[Obsolete("Please use the constructor taking all parameters. Scheduled for removal in Umbraco 17.")]
public UserPresentationFactory(
IEntityService entityService,
@@ -50,7 +56,7 @@ public class UserPresentationFactory : IUserPresentationFactory
IOptionsSnapshot<SecuritySettings> securitySettings,
IBackOfficeExternalLoginProviders externalLoginProviders)
: this(
entityService,
entityService,
appCaches,
mediaFileManager,
imageUrlGenerator,
@@ -61,10 +67,15 @@ public class UserPresentationFactory : IUserPresentationFactory
securitySettings,
externalLoginProviders,
StaticServiceProvider.Instance.GetRequiredService<IUserService>(),
StaticServiceProvider.Instance.GetRequiredService<IContentService>())
StaticServiceProvider.Instance.GetRequiredService<IContentService>(),
StaticServiceProvider.Instance.GetRequiredService<IEnumerable<IPermissionPresentationMapper>>())
{
}
/// <summary>
/// Initializes a new instance of the <see cref="UserPresentationFactory"/> class.
/// </summary>
[Obsolete("Please use the constructor taking all parameters. Scheduled for removal in Umbraco 17.")]
public UserPresentationFactory(
IEntityService entityService,
AppCaches appCaches,
@@ -78,6 +89,44 @@ public class UserPresentationFactory : IUserPresentationFactory
IBackOfficeExternalLoginProviders externalLoginProviders,
IUserService userService,
IContentService contentService)
: this(
entityService,
appCaches,
mediaFileManager,
imageUrlGenerator,
userGroupPresentationFactory,
absoluteUrlBuilder,
emailSender,
passwordConfigurationPresentationFactory,
securitySettings,
externalLoginProviders,
userService,
contentService,
StaticServiceProvider.Instance.GetRequiredService<IEnumerable<IPermissionPresentationMapper>>())
{
}
// TODO (V17): Remove the unused userService and contentService parameters from this constructor.
/// <summary>
/// Initializes a new instance of the <see cref="UserPresentationFactory"/> class.
/// </summary>
public UserPresentationFactory(
IEntityService entityService,
AppCaches appCaches,
MediaFileManager mediaFileManager,
IImageUrlGenerator imageUrlGenerator,
IUserGroupPresentationFactory userGroupPresentationFactory,
IAbsoluteUrlBuilder absoluteUrlBuilder,
IEmailSender emailSender,
IPasswordConfigurationPresentationFactory passwordConfigurationPresentationFactory,
IOptionsSnapshot<SecuritySettings> securitySettings,
IBackOfficeExternalLoginProviders externalLoginProviders,
#pragma warning disable IDE0060 // Remove unused parameter - need to keep these until the next major to avoid breaking changes and/or ambiguous constructor errors
IUserService userService,
IContentService contentService,
#pragma warning restore IDE0060 // Remove unused parameter
IEnumerable<IPermissionPresentationMapper> permissionPresentationMappers)
{
_entityService = entityService;
_appCaches = appCaches;
@@ -89,10 +138,10 @@ public class UserPresentationFactory : IUserPresentationFactory
_externalLoginProviders = externalLoginProviders;
_securitySettings = securitySettings.Value;
_absoluteUrlBuilder = absoluteUrlBuilder;
_userService = userService;
_contentService = contentService;
_permissionPresentationMappersByType = permissionPresentationMappers.ToDictionary(x => x.PresentationModelToHandle);
}
/// <inheritdoc/>
public UserResponseModel CreateResponseModel(IUser user)
{
var responseModel = new UserResponseModel
@@ -123,6 +172,7 @@ public class UserPresentationFactory : IUserPresentationFactory
return responseModel;
}
/// <inheritdoc/>
public UserItemResponseModel CreateItemResponseModel(IUser user) =>
new()
{
@@ -130,9 +180,10 @@ public class UserPresentationFactory : IUserPresentationFactory
Name = user.Name ?? user.Username,
AvatarUrls = user.GetUserAvatarUrls(_appCaches.RuntimeCache, _mediaFileManager, _imageUrlGenerator)
.Select(url => _absoluteUrlBuilder.ToAbsoluteUrl(url).ToString()),
Kind = user.Kind
Kind = user.Kind,
};
/// <inheritdoc/>
public Task<UserCreateModel> CreateCreationModelAsync(CreateUserRequestModel requestModel)
{
var createModel = new UserCreateModel
@@ -142,12 +193,13 @@ public class UserPresentationFactory : IUserPresentationFactory
Name = requestModel.Name,
UserName = requestModel.UserName,
UserGroupKeys = requestModel.UserGroupIds.Select(x => x.Id).ToHashSet(),
Kind = requestModel.Kind
Kind = requestModel.Kind,
};
return Task.FromResult(createModel);
}
/// <inheritdoc/>
public Task<UserInviteModel> CreateInviteModelAsync(InviteUserRequestModel requestModel)
{
var inviteModel = new UserInviteModel
@@ -162,6 +214,7 @@ public class UserPresentationFactory : IUserPresentationFactory
return Task.FromResult(inviteModel);
}
/// <inheritdoc/>
public Task<UserResendInviteModel> CreateResendInviteModelAsync(ResendInviteUserRequestModel requestModel)
{
var inviteModel = new UserResendInviteModel
@@ -173,6 +226,7 @@ public class UserPresentationFactory : IUserPresentationFactory
return Task.FromResult(inviteModel);
}
/// <inheritdoc/>
public Task<CurrentUserConfigurationResponseModel> CreateCurrentUserConfigurationModelAsync()
{
var model = new CurrentUserConfigurationResponseModel
@@ -188,6 +242,7 @@ public class UserPresentationFactory : IUserPresentationFactory
return Task.FromResult(model);
}
/// <inheritdoc/>
public Task<UserConfigurationResponseModel> CreateUserConfigurationModelAsync() =>
Task.FromResult(new UserConfigurationResponseModel
{
@@ -201,6 +256,7 @@ public class UserPresentationFactory : IUserPresentationFactory
AllowTwoFactor = _externalLoginProviders.HasDenyLocalLogin() is false,
});
/// <inheritdoc/>
public Task<UserUpdateModel> CreateUpdateModelAsync(Guid existingUserKey, UpdateUserRequestModel updateModel)
{
var model = new UserUpdateModel
@@ -214,24 +270,24 @@ public class UserPresentationFactory : IUserPresentationFactory
HasContentRootAccess = updateModel.HasDocumentRootAccess,
MediaStartNodeKeys = updateModel.MediaStartNodeIds.Select(x => x.Id).ToHashSet(),
HasMediaRootAccess = updateModel.HasMediaRootAccess,
UserGroupKeys = updateModel.UserGroupIds.Select(x => x.Id).ToHashSet()
};
model.UserGroupKeys = updateModel.UserGroupIds.Select(x => x.Id).ToHashSet();
return Task.FromResult(model);
}
/// <inheritdoc/>
public async Task<CurrentUserResponseModel> CreateCurrentUserResponseModelAsync(IUser user)
{
var presentationUser = CreateResponseModel(user);
var presentationGroups = await _userGroupPresentationFactory.CreateMultipleAsync(user.Groups);
UserResponseModel presentationUser = CreateResponseModel(user);
IEnumerable<UserGroupResponseModel> presentationGroups = await _userGroupPresentationFactory.CreateMultipleAsync(user.Groups);
var languages = presentationGroups.SelectMany(x => x.Languages).Distinct().ToArray();
var mediaStartNodeIds = user.CalculateMediaStartNodeIds(_entityService, _appCaches);
var mediaStartNodeKeys = GetKeysFromIds(mediaStartNodeIds, UmbracoObjectTypes.Media);
ISet<ReferenceByIdModel> mediaStartNodeKeys = GetKeysFromIds(mediaStartNodeIds, UmbracoObjectTypes.Media);
var contentStartNodeIds = user.CalculateContentStartNodeIds(_entityService, _appCaches);
var documentStartNodeKeys = GetKeysFromIds(contentStartNodeIds, UmbracoObjectTypes.Document);
ISet<ReferenceByIdModel> documentStartNodeKeys = GetKeysFromIds(contentStartNodeIds, UmbracoObjectTypes.Document);
var permissions = GetAggregatedGranularPermissions(user, presentationGroups);
HashSet<IPermissionPresentationModel> permissions = GetAggregatedGranularPermissions(user, presentationGroups);
var fallbackPermissions = presentationGroups.SelectMany(x => x.FallbackPermissions).ToHashSet();
var hasAccessToAllLanguages = presentationGroups.Any(x => x.HasAccessToAllLanguages);
@@ -263,70 +319,56 @@ public class UserPresentationFactory : IUserPresentationFactory
private HashSet<IPermissionPresentationModel> GetAggregatedGranularPermissions(IUser user, IEnumerable<UserGroupResponseModel> presentationGroups)
{
var aggregatedPermissions = new HashSet<IPermissionPresentationModel>();
var permissions = presentationGroups.SelectMany(x => x.Permissions).ToHashSet();
return GetAggregatedGranularPermissions(user, permissions);
}
AggregateAndAddDocumentPermissions(user, aggregatedPermissions, permissions);
private HashSet<IPermissionPresentationModel> GetAggregatedGranularPermissions(IUser user, HashSet<IPermissionPresentationModel> permissions)
{
// The raw permission data consists of several permissions for each entity (e.g. document), as permissions are assigned to user groups
// and a user may be part of multiple groups. We want to aggregate this server-side so we return one set of aggregate permissions per
// entity that the client will use.
// We need to handle here not just permissions known to core (e.g. document and document property value permissions), but also custom
// permissions defined by packages or implemetors.
IEnumerable<(Type, IEnumerable<IPermissionPresentationModel>)> permissionModelsByType = permissions
.GroupBy(x => x.GetType())
.Select(x => (x.Key, x.Select(y => y)));
AggregateAndAddDocumentPropertyValuePermissions(aggregatedPermissions, permissions);
var aggregatedPermissions = new HashSet<IPermissionPresentationModel>();
foreach ((Type Type, IEnumerable<IPermissionPresentationModel> Models) permissionModelByType in permissionModelsByType)
{
if (_permissionPresentationMappersByType.TryGetValue(permissionModelByType.Type, out IPermissionPresentationMapper? mapper))
{
IEnumerable<IPermissionPresentationModel> aggregatedModels = mapper.AggregatePresentationModels(user, permissionModelByType.Models);
foreach (IPermissionPresentationModel aggregatedModel in aggregatedModels)
{
aggregatedPermissions.Add(aggregatedModel);
}
}
else
{
IEnumerable<(string Context, ISet<string> Verbs)> groupedModels = permissionModelByType.Models
.Where(x => x is UnknownTypePermissionPresentationModel)
.Cast<UnknownTypePermissionPresentationModel>()
.GroupBy(x => x.Context)
.Select(x => (x.Key, (ISet<string>)x.SelectMany(y => y.Verbs).Distinct().ToHashSet()));
foreach ((string context, ISet<string> verbs) in groupedModels)
{
aggregatedPermissions.Add(new UnknownTypePermissionPresentationModel
{
Context = context,
Verbs = verbs
});
}
}
}
return aggregatedPermissions;
}
private void AggregateAndAddDocumentPermissions(IUser user, HashSet<IPermissionPresentationModel> aggregatedPermissions, HashSet<IPermissionPresentationModel> permissions)
{
// The raw permission data consists of several permissions for each document. We want to aggregate this server-side so
// we return one set of aggregate permissions per document that the client will use.
// Get the unique document keys that have granular permissions.
IEnumerable<Guid> documentKeysWithGranularPermissions = permissions
.Where(x => x is DocumentPermissionPresentationModel)
.Cast<DocumentPermissionPresentationModel>()
.Select(x => x.Document.Id)
.Distinct();
foreach (Guid documentKey in documentKeysWithGranularPermissions)
{
// Retrieve the path of the document.
var path = _contentService.GetById(documentKey)?.Path;
if (string.IsNullOrEmpty(path))
{
continue;
}
// With the path we can call the same logic as used server-side for authorizing access to resources.
EntityPermissionSet permissionsForPath = _userService.GetPermissionsForPath(user, path);
aggregatedPermissions.Add(new DocumentPermissionPresentationModel
{
Document = new ReferenceByIdModel(documentKey),
Verbs = permissionsForPath.GetAllPermissions()
});
}
}
private static void AggregateAndAddDocumentPropertyValuePermissions(HashSet<IPermissionPresentationModel> aggregatedPermissions, HashSet<IPermissionPresentationModel> permissions)
{
// We also have permissions for document type/property type combinations.
// These don't have an ancestor relationship that we need to take into account, but should be aggregated
// and included in the set.
IEnumerable<((Guid DocumentTypeId, Guid PropertyTypeId) Key, ISet<string> Verbs)> documentTypePropertyTypeKeysWithGranularPermissions = permissions
.Where(x => x is DocumentPropertyValuePermissionPresentationModel)
.Cast<DocumentPropertyValuePermissionPresentationModel>()
.GroupBy(x => (x.DocumentType.Id, x.PropertyType.Id))
.Select(x => (x.Key, (ISet<string>)x.SelectMany(y => y.Verbs).Distinct().ToHashSet()));
foreach (((Guid DocumentTypeId, Guid PropertyTypeId) Key, ISet<string> Verbs) documentTypePropertyTypeKey in documentTypePropertyTypeKeysWithGranularPermissions)
{
aggregatedPermissions.Add(new DocumentPropertyValuePermissionPresentationModel
{
DocumentType = new ReferenceByIdModel(documentTypePropertyTypeKey.Key.DocumentTypeId),
PropertyType = new ReferenceByIdModel(documentTypePropertyTypeKey.Key.PropertyTypeId),
Verbs = documentTypePropertyTypeKey.Verbs
});
}
}
/// <inheritdoc/>
public Task<CalculatedUserStartNodesResponseModel> CreateCalculatedUserStartNodesResponseModelAsync(IUser user)
{
var mediaStartNodeIds = user.CalculateMediaStartNodeIds(_entityService, _appCaches);
@@ -357,6 +399,6 @@ public class UserPresentationFactory : IUserPresentationFactory
: new HashSet<ReferenceByIdModel>(models);
}
private bool HasRootAccess(IEnumerable<int>? startNodeIds)
private static bool HasRootAccess(IEnumerable<int>? startNodeIds)
=> startNodeIds?.Contains(Constants.System.Root) is true;
}
@@ -58,6 +58,7 @@ public class PackageViewModelMapDefinition : IMapDefinition
}
// Umbraco.Code.MapAll
[Obsolete("Please use the IPackagePresentationFactory instead. Scheduled for removal in V18.")]
private static void Map(InstalledPackage source, PackageMigrationStatusResponseModel target, MapperContext context)
{
if (source.PackageName is not null)
@@ -1,19 +1,50 @@
using Microsoft.Extensions.DependencyInjection;
using Umbraco.Cms.Api.Management.ViewModels;
using Umbraco.Cms.Api.Management.ViewModels.UserGroup.Permissions;
using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.Membership;
using Umbraco.Cms.Core.Models.Membership.Permissions;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Infrastructure.Persistence.Dtos;
using Umbraco.Cms.Infrastructure.Persistence.Mappers;
namespace Umbraco.Cms.Api.Management.Mapping.Permissions;
/// <summary>
/// Mapping required for mapping all the way from viewmodel to database and back.
/// Implements <see cref="IPermissionPresentationMapper" /> for document permissions.
/// </summary>
/// <remarks>
/// This mapping maps all the way from management api to database in one file intentionally, so it is very clear what it takes, if we wanna add permissions to media or other types in the future.
/// </remarks>
public class DocumentPermissionMapper : IPermissionPresentationMapper, IPermissionMapper
{
private readonly Lazy<IEntityService> _entityService;
private readonly Lazy<IUserService> _userService;
/// <summary>
/// Initializes a new instance of the <see cref="DocumentPermissionMapper"/> class.
/// </summary>
[Obsolete("Please use the constructor taking all parameters. Scheduled for removal in Umbraco 17.")]
public DocumentPermissionMapper()
: this(
StaticServiceProvider.Instance.GetRequiredService<Lazy<IEntityService>>(),
StaticServiceProvider.Instance.GetRequiredService<Lazy<IUserService>>())
{
}
/// <summary>
/// Initializes a new instance of the <see cref="DocumentPermissionMapper"/> class.
/// </summary>
public DocumentPermissionMapper(Lazy<IEntityService> entityService, Lazy<IUserService> userService)
{
_entityService = entityService;
_userService = userService;
}
/// <inheritdoc/>
public string Context => DocumentGranularPermission.ContextType;
public IGranularPermission MapFromDto(UserGroup2GranularPermissionDto dto) =>
new DocumentGranularPermission()
{
@@ -21,8 +52,10 @@ public class DocumentPermissionMapper : IPermissionPresentationMapper, IPermissi
Permission = dto.Permission,
};
/// <inheritdoc/>
public Type PresentationModelToHandle => typeof(DocumentPermissionPresentationModel);
/// <inheritdoc/>
public IEnumerable<IPermissionPresentationModel> MapManyAsync(IEnumerable<IGranularPermission> granularPermissions)
{
IEnumerable<IGrouping<Guid?, IGranularPermission>> keyGroups = granularPermissions.GroupBy(x => x.Key);
@@ -40,6 +73,7 @@ public class DocumentPermissionMapper : IPermissionPresentationMapper, IPermissi
}
}
/// <inheritdoc/>
public IEnumerable<IGranularPermission> MapToGranularPermissions(IPermissionPresentationModel permissionViewModel)
{
if (permissionViewModel is not DocumentPermissionPresentationModel documentPermissionPresentationModel)
@@ -47,7 +81,7 @@ public class DocumentPermissionMapper : IPermissionPresentationMapper, IPermissi
yield break;
}
if(documentPermissionPresentationModel.Verbs.Any() is false || (documentPermissionPresentationModel.Verbs.Count == 1 && documentPermissionPresentationModel.Verbs.Contains(string.Empty)))
if (documentPermissionPresentationModel.Verbs.Any() is false || (documentPermissionPresentationModel.Verbs.Count == 1 && documentPermissionPresentationModel.Verbs.Contains(string.Empty)))
{
yield return new DocumentGranularPermission
{
@@ -56,12 +90,14 @@ public class DocumentPermissionMapper : IPermissionPresentationMapper, IPermissi
};
yield break;
}
foreach (var verb in documentPermissionPresentationModel.Verbs)
{
if (string.IsNullOrEmpty(verb))
{
continue;
}
yield return new DocumentGranularPermission
{
Key = documentPermissionPresentationModel.Document.Id,
@@ -69,4 +105,37 @@ public class DocumentPermissionMapper : IPermissionPresentationMapper, IPermissi
};
}
}
/// <inheritdoc/>
public IEnumerable<IPermissionPresentationModel> AggregatePresentationModels(IUser user, IEnumerable<IPermissionPresentationModel> models)
{
// Get the unique document keys that have granular permissions.
Guid[] documentKeysWithGranularPermissions = models
.Cast<DocumentPermissionPresentationModel>()
.Select(x => x.Document.Id)
.Distinct()
.ToArray();
// Batch retrieve all documents by their keys.
var documents = _entityService.Value.GetAll<IContent>(documentKeysWithGranularPermissions)
.ToDictionary(doc => doc.Key, doc => doc.Path);
// Iterate through each document key that has granular permissions.
foreach (Guid documentKey in documentKeysWithGranularPermissions)
{
// Retrieve the path from the pre-fetched documents.
if (!documents.TryGetValue(documentKey, out var path) || string.IsNullOrEmpty(path))
{
continue;
}
// With the path we can call the same logic as used server-side for authorizing access to resources.
EntityPermissionSet permissionsForPath = _userService.Value.GetPermissionsForPath(user, path);
yield return new DocumentPermissionPresentationModel
{
Document = new ReferenceByIdModel(documentKey),
Verbs = permissionsForPath.GetAllPermissions(),
};
}
}
}
@@ -1,5 +1,6 @@
using Umbraco.Cms.Api.Management.ViewModels;
using Umbraco.Cms.Api.Management.ViewModels;
using Umbraco.Cms.Api.Management.ViewModels.UserGroup.Permissions;
using Umbraco.Cms.Core.Models.Membership;
using Umbraco.Cms.Core.Models.Membership.Permissions;
using Umbraco.Cms.Infrastructure.Persistence.Dtos;
using Umbraco.Cms.Infrastructure.Persistence.Mappers;
@@ -7,10 +8,18 @@ using Umbraco.Extensions;
namespace Umbraco.Cms.Api.Management.Mapping.Permissions;
/// <summary>
/// Implements <see cref="IPermissionPresentationMapper" /> for document property value permissions.
/// </summary>
public class DocumentPropertyValuePermissionMapper : IPermissionPresentationMapper, IPermissionMapper
{
/// <inheritdoc/>
public string Context => DocumentPropertyValueGranularPermission.ContextType;
/// <inheritdoc/>
public Type PresentationModelToHandle => typeof(DocumentPropertyValuePermissionPresentationModel);
/// <inheritdoc/>
public IGranularPermission MapFromDto(UserGroup2GranularPermissionDto dto) =>
new DocumentPropertyValueGranularPermission()
{
@@ -18,8 +27,7 @@ public class DocumentPropertyValuePermissionMapper : IPermissionPresentationMapp
Permission = dto.Permission,
};
public Type PresentationModelToHandle => typeof(DocumentPropertyValuePermissionPresentationModel);
/// <inheritdoc/>
public IEnumerable<IPermissionPresentationModel> MapManyAsync(IEnumerable<IGranularPermission> granularPermissions)
{
var intermediate = granularPermissions.Where(p => p.Key.HasValue).Select(p =>
@@ -50,6 +58,7 @@ public class DocumentPropertyValuePermissionMapper : IPermissionPresentationMapp
}
}
/// <inheritdoc/>
public IEnumerable<IGranularPermission> MapToGranularPermissions(IPermissionPresentationModel permissionViewModel)
{
if (permissionViewModel is not DocumentPropertyValuePermissionPresentationModel documentTypePermissionPresentationModel)
@@ -66,4 +75,23 @@ public class DocumentPropertyValuePermissionMapper : IPermissionPresentationMapp
};
}
}
/// <inheritdoc/>
public IEnumerable<IPermissionPresentationModel> AggregatePresentationModels(IUser user, IEnumerable<IPermissionPresentationModel> models)
{
IEnumerable<((Guid DocumentTypeId, Guid PropertyTypeId) Key, ISet<string> Verbs)> groupedModels = models
.Cast<DocumentPropertyValuePermissionPresentationModel>()
.GroupBy(x => (x.DocumentType.Id, x.PropertyType.Id))
.Select(x => (x.Key, (ISet<string>)x.SelectMany(y => y.Verbs).Distinct().ToHashSet()));
foreach (((Guid DocumentTypeId, Guid PropertyTypeId) key, ISet<string> verbs) in groupedModels)
{
yield return new DocumentPropertyValuePermissionPresentationModel
{
DocumentType = new ReferenceByIdModel(key.DocumentTypeId),
PropertyType = new ReferenceByIdModel(key.PropertyTypeId),
Verbs = verbs
};
}
}
}
@@ -1,15 +1,36 @@
using Umbraco.Cms.Api.Management.ViewModels.UserGroup.Permissions;
using Umbraco.Cms.Core.Models.Membership;
using Umbraco.Cms.Core.Models.Membership.Permissions;
namespace Umbraco.Cms.Api.Management.Mapping.Permissions;
/// <summary>
/// Defines methods for mapping and aggregating granular permissions to presentation models.
/// </summary>
public interface IPermissionPresentationMapper
{
/// <summary>
/// Gets the context type for the permissions being handled by this mapper.
/// </summary>
string Context { get; }
/// <summary>
/// Gets the type of the presentation model that this mapper handles.
/// </summary>
Type PresentationModelToHandle { get; }
/// <summary>
/// Maps a granular permission entity to a granular permission model.
/// </summary>
IEnumerable<IPermissionPresentationModel> MapManyAsync(IEnumerable<IGranularPermission> granularPermissions);
/// <summary>
/// Maps a granular permission to a granular permission model.
/// </summary>
IEnumerable<IGranularPermission> MapToGranularPermissions(IPermissionPresentationModel permissionViewModel);
/// <summary>
/// Aggregates multiple permission presentation models into a collection containing only one item per entity with aggregated permissions.
/// </summary>
IEnumerable<IPermissionPresentationModel> AggregatePresentationModels(IUser user, IEnumerable<IPermissionPresentationModel> models) => [];
}
File diff suppressed because it is too large Load Diff
+4 -2
View File
@@ -41,7 +41,7 @@ public static class AppCacheExtensions
public static T? GetCacheItem<T>(this IAppCache provider, string cacheKey)
{
var result = provider.Get(cacheKey);
if (result == null)
if (IsRetrievedItemNull(result))
{
return default;
}
@@ -52,7 +52,7 @@ public static class AppCacheExtensions
public static T? GetCacheItem<T>(this IAppCache provider, string cacheKey, Func<T> getCacheItem)
{
var result = provider.Get(cacheKey, () => getCacheItem());
if (result == null)
if (IsRetrievedItemNull(result))
{
return default;
}
@@ -60,6 +60,8 @@ public static class AppCacheExtensions
return result.TryConvertTo<T>().Result;
}
private static bool IsRetrievedItemNull(object? result) => result is null or (object)Cms.Core.Constants.Cache.NullRepresentationInCache;
public static async Task<T?> GetCacheItemAsync<T>(
this IAppPolicyCache provider,
string cacheKey,
@@ -4,6 +4,7 @@
using Umbraco.Cms.Core.Cache;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.Membership;
using Umbraco.Cms.Core.Notifications;
using Umbraco.Cms.Core.Services.Changes;
namespace Umbraco.Extensions;
@@ -152,11 +153,49 @@ public static class DistributedCacheExtensions
#region MemberCacheRefresher
[Obsolete("Please use the overload taking all parameters. Scheduled for removal in Umbraco 18.")]
public static void RefreshMemberCache(this DistributedCache dc, IEnumerable<IMember> members)
=> dc.RefreshByPayload(MemberCacheRefresher.UniqueId, members.DistinctBy(x => (x.Id, x.Username)).Select(x => new MemberCacheRefresher.JsonPayload(x.Id, x.Username, false)));
=> dc.RefreshMemberCache(members, new Dictionary<string, object?>());
public static void RefreshMemberCache(this DistributedCache dc, IEnumerable<IMember> members, IDictionary<string, object?> state)
=> dc.RefreshByPayload(
MemberCacheRefresher.UniqueId,
GetPayloads(members, state, false));
[Obsolete("Please use the overload taking all parameters. Scheduled for removal in Umbraco 18.")]
public static void RemoveMemberCache(this DistributedCache dc, IEnumerable<IMember> members)
=> dc.RefreshByPayload(MemberCacheRefresher.UniqueId, members.DistinctBy(x => (x.Id, x.Username)).Select(x => new MemberCacheRefresher.JsonPayload(x.Id, x.Username, true)));
=> dc.RemoveMemberCache(members, new Dictionary<string, object?>());
public static void RemoveMemberCache(this DistributedCache dc, IEnumerable<IMember> members, IDictionary<string, object?> state)
=> dc.RefreshByPayload(
MemberCacheRefresher.UniqueId,
GetPayloads(members, state, true));
// Internal for unit test.
internal static IEnumerable<MemberCacheRefresher.JsonPayload> GetPayloads(IEnumerable<IMember> members, IDictionary<string, object?> state, bool removed)
=> members
.DistinctBy(x => (x.Id, x.Username))
.Select(x => new MemberCacheRefresher.JsonPayload(x.Id, x.Username, removed)
{
PreviousUsername = GetPreviousUsername(x, state)
});
private static string? GetPreviousUsername(IMember x, IDictionary<string, object?> state)
{
if (state.TryGetValue(MemberSavedNotification.PreviousUsernameStateKey, out object? previousUserNames) is false)
{
return null;
}
if (previousUserNames is not IDictionary<Guid, string> previousUserNamesDictionary)
{
return null;
}
return previousUserNamesDictionary.TryGetValue(x.Key, out string? previousUsername)
? previousUsername
: null;
}
#endregion
@@ -10,11 +10,20 @@ public abstract class DistributedCacheNotificationHandlerBase<TEntity, TNotifica
{
/// <inheritdoc />
public void Handle(TNotification notification)
=> Handle(GetEntities(notification));
=> Handle(
GetEntities(notification),
notification is StatefulNotification statefulNotification
? statefulNotification.State
: new Dictionary<string, object?>());
/// <inheritdoc />
public void Handle(IEnumerable<TNotification> notifications)
=> Handle(notifications.SelectMany(GetEntities));
{
foreach (TNotification notification in notifications)
{
Handle(notification);
}
}
/// <summary>
/// Gets the entities from the specified notification.
@@ -25,9 +34,23 @@ public abstract class DistributedCacheNotificationHandlerBase<TEntity, TNotifica
/// </returns>
protected abstract IEnumerable<TEntity> GetEntities(TNotification notification);
// TODO (V18): When removing the obsolete method, make the remaining Handle method abstract
// rather than virtual. It couldn't be made abstract when introduced as that would be a breaking change.
/// <summary>
/// Handles the specified entities.
/// </summary>
/// <param name="entities">The entities.</param>
[Obsolete("Please use the overload taking all parameters. Scheduled for removal in Umbraco 18.")]
protected abstract void Handle(IEnumerable<TEntity> entities);
/// <summary>
/// Handles the specified entities.
/// </summary>
/// <param name="entities">The entities.</param>
/// <param name="state">The notification state.</param>
protected virtual void Handle(IEnumerable<TEntity> entities, IDictionary<string, object?> state)
#pragma warning disable CS0618 // Type or member is obsolete
=> Handle(entities);
#pragma warning restore CS0618 // Type or member is obsolete
}
@@ -18,6 +18,11 @@ public sealed class ContentTreeChangeDistributedCacheNotificationHandler : TreeC
=> _distributedCache = distributedCache;
/// <inheritdoc />
[Obsolete("Scheduled for removal in Umbraco 18.")]
protected override void Handle(IEnumerable<TreeChange<IContent>> entities)
=> Handle(entities, new Dictionary<string, object?>());
/// <inheritdoc />
protected override void Handle(IEnumerable<TreeChange<IContent>> entities, IDictionary<string, object?> state)
=> _distributedCache.RefreshContentCache(entities);
}
@@ -18,6 +18,11 @@ public sealed class ContentTypeChangedDistributedCacheNotificationHandler : Cont
=> _distributedCache = distributedCache;
/// <inheritdoc />
[Obsolete("Scheduled for removal in Umbraco 18.")]
protected override void Handle(IEnumerable<ContentTypeChange<IContentType>> entities)
=> Handle(entities, new Dictionary<string, object?>());
/// <inheritdoc />
protected override void Handle(IEnumerable<ContentTypeChange<IContentType>> entities, IDictionary<string, object?> state)
=> _distributedCache.RefreshContentTypeCache(entities);
}
@@ -1,5 +1,6 @@
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Notifications;
using Umbraco.Cms.Core.Services.Changes;
using Umbraco.Extensions;
namespace Umbraco.Cms.Core.Cache;
@@ -17,7 +18,12 @@ public sealed class DataTypeDeletedDistributedCacheNotificationHandler : Deleted
=> _distributedCache = distributedCache;
/// <inheritdoc />
[Obsolete("Scheduled for removal in Umbraco 18.")]
protected override void Handle(IEnumerable<IDataType> entities)
=> Handle(entities, new Dictionary<string, object?>());
/// <inheritdoc />
protected override void Handle(IEnumerable<IDataType> entities, IDictionary<string, object?> state)
{
_distributedCache.RemoveDataTypeCache(entities);
_distributedCache.RefreshValueEditorCache(entities);
@@ -17,7 +17,12 @@ public sealed class DataTypeSavedDistributedCacheNotificationHandler : SavedDist
=> _distributedCache = distributedCache;
/// <inheritdoc />
[Obsolete("Scheduled for removal in Umbraco 18.")]
protected override void Handle(IEnumerable<IDataType> entities)
=> Handle(entities, new Dictionary<string, object?>());
/// <inheritdoc />
protected override void Handle(IEnumerable<IDataType> entities, IDictionary<string, object?> state)
{
_distributedCache.RefreshDataTypeCache(entities);
_distributedCache.RefreshValueEditorCache(entities);
@@ -17,6 +17,11 @@ public sealed class DictionaryItemDeletedDistributedCacheNotificationHandler : D
=> _distributedCache = distributedCache;
/// <inheritdoc />
[Obsolete("Scheduled for removal in Umbraco 18.")]
protected override void Handle(IEnumerable<IDictionaryItem> entities)
=> Handle(entities, new Dictionary<string, object?>());
/// <inheritdoc />
protected override void Handle(IEnumerable<IDictionaryItem> entities, IDictionary<string, object?> state)
=> _distributedCache.RemoveDictionaryCache(entities);
}
@@ -17,6 +17,11 @@ public sealed class DictionaryItemSavedDistributedCacheNotificationHandler : Sav
=> _distributedCache = distributedCache;
/// <inheritdoc />
[Obsolete("Scheduled for removal in Umbraco 18.")]
protected override void Handle(IEnumerable<IDictionaryItem> entities)
=> Handle(entities, new Dictionary<string, object?>());
/// <inheritdoc />
protected override void Handle(IEnumerable<IDictionaryItem> entities, IDictionary<string, object?> state)
=> _distributedCache.RefreshDictionaryCache(entities);
}
@@ -17,6 +17,11 @@ public sealed class DomainDeletedDistributedCacheNotificationHandler : DeletedDi
=> _distributedCache = distributedCache;
/// <inheritdoc />
[Obsolete("Scheduled for removal in Umbraco 18.")]
protected override void Handle(IEnumerable<IDomain> entities)
=> Handle(entities, new Dictionary<string, object?>());
/// <inheritdoc />
protected override void Handle(IEnumerable<IDomain> entities, IDictionary<string, object?> state)
=> _distributedCache.RemoveDomainCache(entities);
}
@@ -17,6 +17,11 @@ public sealed class DomainSavedDistributedCacheNotificationHandler : SavedDistri
=> _distributedCache = distributedCache;
/// <inheritdoc />
[Obsolete("Scheduled for removal in Umbraco 18.")]
protected override void Handle(IEnumerable<IDomain> entities)
=> Handle(entities, new Dictionary<string, object?>());
/// <inheritdoc />
protected override void Handle(IEnumerable<IDomain> entities, IDictionary<string, object?> state)
=> _distributedCache.RefreshDomainCache(entities);
}
@@ -17,6 +17,11 @@ public sealed class LanguageDeletedDistributedCacheNotificationHandler : Deleted
=> _distributedCache = distributedCache;
/// <inheritdoc />
[Obsolete("Scheduled for removal in Umbraco 18.")]
protected override void Handle(IEnumerable<ILanguage> entities)
=> Handle(entities, new Dictionary<string, object?>());
/// <inheritdoc />
protected override void Handle(IEnumerable<ILanguage> entities, IDictionary<string, object?> state)
=> _distributedCache.RemoveLanguageCache(entities);
}
@@ -17,6 +17,11 @@ public sealed class LanguageSavedDistributedCacheNotificationHandler : SavedDist
=> _distributedCache = distributedCache;
/// <inheritdoc />
[Obsolete("Scheduled for removal in Umbraco 18.")]
protected override void Handle(IEnumerable<ILanguage> entities)
=> Handle(entities, new Dictionary<string, object?>());
/// <inheritdoc />
protected override void Handle(IEnumerable<ILanguage> entities, IDictionary<string, object?> state)
=> _distributedCache.RefreshLanguageCache(entities);
}
@@ -18,6 +18,11 @@ public sealed class MediaTreeChangeDistributedCacheNotificationHandler : TreeCha
=> _distributedCache = distributedCache;
/// <inheritdoc />
[Obsolete("Scheduled for removal in Umbraco 18.")]
protected override void Handle(IEnumerable<TreeChange<IMedia>> entities)
=> Handle(entities, new Dictionary<string, object?>());
/// <inheritdoc />
protected override void Handle(IEnumerable<TreeChange<IMedia>> entities, IDictionary<string, object?> state)
=> _distributedCache.RefreshMediaCache(entities);
}
@@ -18,6 +18,11 @@ public sealed class MediaTypeChangedDistributedCacheNotificationHandler : Conten
=> _distributedCache = distributedCache;
/// <inheritdoc />
[Obsolete("Scheduled for removal in Umbraco 18.")]
protected override void Handle(IEnumerable<ContentTypeChange<IMediaType>> entities)
=> Handle(entities, new Dictionary<string, object?>());
/// <inheritdoc />
protected override void Handle(IEnumerable<ContentTypeChange<IMediaType>> entities, IDictionary<string, object?> state)
=> _distributedCache.RefreshContentTypeCache(entities);
}
@@ -1,5 +1,6 @@
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Notifications;
using Umbraco.Cms.Core.Services.Changes;
using Umbraco.Extensions;
namespace Umbraco.Cms.Core.Cache;
@@ -17,6 +18,11 @@ public sealed class MemberDeletedDistributedCacheNotificationHandler : DeletedDi
=> _distributedCache = distributedCache;
/// <inheritdoc />
[Obsolete("Scheduled for removal in Umbraco 18.")]
protected override void Handle(IEnumerable<IMember> entities)
=> _distributedCache.RemoveMemberCache(entities);
=> Handle(entities, new Dictionary<string, object?>());
/// <inheritdoc />
protected override void Handle(IEnumerable<IMember> entities, IDictionary<string, object?> state)
=> _distributedCache.RemoveMemberCache(entities, state);
}
@@ -17,6 +17,11 @@ public sealed class MemberGroupDeletedDistributedCacheNotificationHandler : Dele
=> _distributedCache = distributedCache;
/// <inheritdoc />
[Obsolete("Scheduled for removal in Umbraco 18.")]
protected override void Handle(IEnumerable<IMemberGroup> entities)
=> Handle(entities, new Dictionary<string, object?>());
/// <inheritdoc />
protected override void Handle(IEnumerable<IMemberGroup> entities, IDictionary<string, object?> state)
=> _distributedCache.RemoveMemberGroupCache(entities);
}
@@ -1,4 +1,4 @@
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Notifications;
using Umbraco.Extensions;
@@ -17,6 +17,11 @@ public sealed class MemberGroupSavedDistributedCacheNotificationHandler : SavedD
=> _distributedCache = distributedCache;
/// <inheritdoc />
[Obsolete("Scheduled for removal in Umbraco 18.")]
protected override void Handle(IEnumerable<IMemberGroup> entities)
=> Handle(entities, new Dictionary<string, object?>());
/// <inheritdoc />
protected override void Handle(IEnumerable<IMemberGroup> entities, IDictionary<string, object?> state)
=> _distributedCache.RefreshMemberGroupCache(entities);
}
@@ -17,6 +17,11 @@ public sealed class MemberSavedDistributedCacheNotificationHandler : SavedDistri
=> _distributedCache = distributedCache;
/// <inheritdoc />
[Obsolete("Scheduled for removal in Umbraco 18.")]
protected override void Handle(IEnumerable<IMember> entities)
=> _distributedCache.RefreshMemberCache(entities);
=> Handle(entities, new Dictionary<string, object?>());
/// <inheritdoc />
protected override void Handle(IEnumerable<IMember> entities, IDictionary<string, object?> state)
=> _distributedCache.RefreshMemberCache(entities, state);
}
@@ -19,6 +19,11 @@ public sealed class MemberTypeChangedDistributedCacheNotificationHandler : Conte
=> _distributedCache = distributedCache;
/// <inheritdoc />
[Obsolete("Scheduled for removal in Umbraco 18.")]
protected override void Handle(IEnumerable<ContentTypeChange<IMemberType>> entities)
=> Handle(entities, new Dictionary<string, object?>());
/// <inheritdoc />
protected override void Handle(IEnumerable<ContentTypeChange<IMemberType>> entities, IDictionary<string, object?> state)
=> _distributedCache.RefreshContentTypeCache(entities);
}
@@ -1,5 +1,6 @@
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Notifications;
using Umbraco.Cms.Core.Services.Changes;
using Umbraco.Extensions;
namespace Umbraco.Cms.Core.Cache;
@@ -17,6 +18,11 @@ public sealed class PublicAccessEntryDeletedDistributedCacheNotificationHandler
=> _distributedCache = distributedCache;
/// <inheritdoc />
[Obsolete("Scheduled for removal in Umbraco 18.")]
protected override void Handle(IEnumerable<PublicAccessEntry> entities)
=> Handle(entities, new Dictionary<string, object?>());
/// <inheritdoc />
protected override void Handle(IEnumerable<PublicAccessEntry> entities, IDictionary<string, object?> state)
=> _distributedCache.RefreshPublicAccess();
}
@@ -17,6 +17,11 @@ public sealed class PublicAccessEntrySavedDistributedCacheNotificationHandler :
=> _distributedCache = distributedCache;
/// <inheritdoc />
[Obsolete("Scheduled for removal in Umbraco 18.")]
protected override void Handle(IEnumerable<PublicAccessEntry> entities)
=> Handle(entities, new Dictionary<string, object?>());
/// <inheritdoc />
protected override void Handle(IEnumerable<PublicAccessEntry> entities, IDictionary<string, object?> state)
=> _distributedCache.RefreshPublicAccess();
}
@@ -17,6 +17,11 @@ public sealed class RelationTypeDeletedDistributedCacheNotificationHandler : Del
=> _distributedCache = distributedCache;
/// <inheritdoc />
[Obsolete("Scheduled for removal in Umbraco 18.")]
protected override void Handle(IEnumerable<IRelationType> entities)
=> Handle(entities, new Dictionary<string, object?>());
/// <inheritdoc />
protected override void Handle(IEnumerable<IRelationType> entities, IDictionary<string, object?> state)
=> _distributedCache.RemoveRelationTypeCache(entities);
}
@@ -17,6 +17,11 @@ public sealed class RelationTypeSavedDistributedCacheNotificationHandler : Saved
=> _distributedCache = distributedCache;
/// <inheritdoc />
[Obsolete("Scheduled for removal in Umbraco 18.")]
protected override void Handle(IEnumerable<IRelationType> entities)
=> Handle(entities, new Dictionary<string, object?>());
/// <inheritdoc />
protected override void Handle(IEnumerable<IRelationType> entities, IDictionary<string, object?> state)
=> _distributedCache.RefreshRelationTypeCache(entities);
}
@@ -17,6 +17,11 @@ public sealed class TemplateDeletedDistributedCacheNotificationHandler : Deleted
=> _distributedCache = distributedCache;
/// <inheritdoc />
[Obsolete("Scheduled for removal in Umbraco 18.")]
protected override void Handle(IEnumerable<ITemplate> entities)
=> Handle(entities, new Dictionary<string, object?>());
/// <inheritdoc />
protected override void Handle(IEnumerable<ITemplate> entities, IDictionary<string, object?> state)
=> _distributedCache.RemoveTemplateCache(entities);
}
@@ -17,6 +17,11 @@ public sealed class TemplateSavedDistributedCacheNotificationHandler : SavedDist
=> _distributedCache = distributedCache;
/// <inheritdoc />
[Obsolete("Scheduled for removal in Umbraco 18.")]
protected override void Handle(IEnumerable<ITemplate> entities)
=> Handle(entities, new Dictionary<string, object?>());
/// <inheritdoc />
protected override void Handle(IEnumerable<ITemplate> entities, IDictionary<string, object?> state)
=> _distributedCache.RefreshTemplateCache(entities);
}
@@ -1,3 +1,4 @@
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.Membership;
using Umbraco.Cms.Core.Notifications;
using Umbraco.Extensions;
@@ -17,6 +18,11 @@ public sealed class UserDeletedDistributedCacheNotificationHandler : DeletedDist
=> _distributedCache = distributedCache;
/// <inheritdoc />
[Obsolete("Scheduled for removal in Umbraco 18.")]
protected override void Handle(IEnumerable<IUser> entities)
=> Handle(entities, new Dictionary<string, object?>());
/// <inheritdoc />
protected override void Handle(IEnumerable<IUser> entities, IDictionary<string, object?> state)
=> _distributedCache.RemoveUserCache(entities);
}
@@ -17,6 +17,11 @@ public sealed class UserGroupDeletedDistributedCacheNotificationHandler : Delete
=> _distributedCache = distributedCache;
/// <inheritdoc />
[Obsolete("Scheduled for removal in Umbraco 18.")]
protected override void Handle(IEnumerable<IUserGroup> entities)
=> Handle(entities, new Dictionary<string, object?>());
/// <inheritdoc />
protected override void Handle(IEnumerable<IUserGroup> entities, IDictionary<string, object?> state)
=> _distributedCache.RemoveUserGroupCache(entities);
}
@@ -22,6 +22,11 @@ public sealed class UserGroupWithUsersSavedDistributedCacheNotificationHandler :
=> notification.SavedEntities.Select(x => x.UserGroup);
/// <inheritdoc />
[Obsolete("Scheduled for removal in Umbraco 18.")]
protected override void Handle(IEnumerable<IUserGroup> entities)
=> Handle(entities, new Dictionary<string, object?>());
/// <inheritdoc />
protected override void Handle(IEnumerable<IUserGroup> entities, IDictionary<string, object?> state)
=> _distributedCache.RefreshUserGroupCache(entities);
}
@@ -17,6 +17,11 @@ public sealed class UserSavedDistributedCacheNotificationHandler : SavedDistribu
=> _distributedCache = distributedCache;
/// <inheritdoc />
[Obsolete("Scheduled for removal in Umbraco 18.")]
protected override void Handle(IEnumerable<IUser> entities)
=> Handle(entities, new Dictionary<string, object?>());
/// <inheritdoc />
protected override void Handle(IEnumerable<IUser> entities, IDictionary<string, object?> state)
=> _distributedCache.RefreshUserCache(entities);
}
@@ -0,0 +1,16 @@
namespace Umbraco.Cms.Core.Cache.PartialViewCacheInvalidators;
/// <summary>
/// Defines behaviours for clearing of cached partials views that are configured to be cached individually by member.
/// </summary>
public interface IMemberPartialViewCacheInvalidator
{
/// <summary>
/// Clears the partial view cache items for the specified member ids.
/// </summary>
/// <param name="memberIds">The member Ids to clear the cache for.</param>
/// <remarks>
/// Called from the <see cref="MemberCacheRefresher"/> when a member is saved or deleted.
/// </remarks>
void ClearPartialViewCacheItems(IEnumerable<int> memberIds);
}
@@ -0,0 +1,17 @@
namespace Umbraco.Cms.Core.Cache.PartialViewCacheInvalidators;
/// <summary>
/// Provides a no-operation implementation of <see cref="IMemberPartialViewCacheInvalidator"/>.
/// </summary>
/// <remarks>
/// 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
{
/// <inheritdoc/>
public void ClearPartialViewCacheItems(IEnumerable<int> memberIds)
{
// No operation performed, this is a no-op implementation.
}
}
@@ -1,10 +1,12 @@
using Microsoft.Extensions.DependencyInjection;
using Umbraco.Cms.Core.Cache.PartialViewCacheInvalidators;
using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Core.Events;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Notifications;
using Umbraco.Cms.Core.Persistence.Repositories;
using Umbraco.Cms.Core.Serialization;
using Umbraco.Cms.Core.Services;
using Umbraco.Extensions;
namespace Umbraco.Cms.Core.Cache;
@@ -13,10 +15,32 @@ public sealed class MemberCacheRefresher : PayloadCacheRefresherBase<MemberCache
public static readonly Guid UniqueId = Guid.Parse("E285DF34-ACDC-4226-AE32-C0CB5CF388DA");
private readonly IIdKeyMap _idKeyMap;
private readonly IMemberPartialViewCacheInvalidator _memberPartialViewCacheInvalidator;
[Obsolete("Use the non obsoleted contructor instead. Planned for removal in V18")]
public MemberCacheRefresher(AppCaches appCaches, IJsonSerializer serializer, IIdKeyMap idKeyMap, IEventAggregator eventAggregator, ICacheRefresherNotificationFactory factory)
: base(appCaches, serializer, eventAggregator, factory) =>
: this(
appCaches,
serializer,
idKeyMap,
eventAggregator,
factory,
StaticServiceProvider.Instance.GetRequiredService<IMemberPartialViewCacheInvalidator>())
{
}
public MemberCacheRefresher(
AppCaches appCaches,
IJsonSerializer serializer,
IIdKeyMap idKeyMap,
IEventAggregator eventAggregator,
ICacheRefresherNotificationFactory factory,
IMemberPartialViewCacheInvalidator memberPartialViewCacheInvalidator)
: base(appCaches, serializer, eventAggregator, factory)
{
_idKeyMap = idKeyMap;
_memberPartialViewCacheInvalidator = memberPartialViewCacheInvalidator;
}
#region Indirect
@@ -38,6 +62,8 @@ public sealed class MemberCacheRefresher : PayloadCacheRefresherBase<MemberCache
public string? Username { get; }
public string? PreviousUsername { get; set; }
public bool Removed { get; }
}
@@ -65,7 +91,8 @@ public sealed class MemberCacheRefresher : PayloadCacheRefresherBase<MemberCache
private void ClearCache(params JsonPayload[] payloads)
{
AppCaches.ClearPartialViewCache();
_memberPartialViewCacheInvalidator.ClearPartialViewCacheItems(payloads.Select(p => p.Id));
Attempt<IAppPolicyCache?> memberCache = AppCaches.IsolatedCaches.Get<IMember>();
foreach (JsonPayload p in payloads)
@@ -87,6 +114,13 @@ public sealed class MemberCacheRefresher : PayloadCacheRefresherBase<MemberCache
// https://github.com/umbraco/Umbraco-CMS/pull/17350
// https://github.com/umbraco/Umbraco-CMS/pull/17815
memberCache.Result?.Clear(RepositoryCacheKeys.GetKey<IMember, string>(CacheKeys.MemberUserNameCachePrefix + p.Username));
// If provided, clear the cache by the previous user name too.
if (string.IsNullOrEmpty(p.PreviousUsername) is false)
{
memberCache.Result?.Clear(RepositoryCacheKeys.GetKey<IMember, string>(p.PreviousUsername));
memberCache.Result?.Clear(RepositoryCacheKeys.GetKey<IMember, string>(CacheKeys.MemberUserNameCachePrefix + p.PreviousUsername));
}
}
}
}
@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- https://learn.microsoft.com/dotnet/fundamentals/package-validation/diagnostic-ids -->
<Suppressions xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Umbraco.Cms.Core.Configuration.Models.ContentSettings.get_Error404Collection</Target>
<Left>lib/net9.0/Umbraco.Core.dll</Left>
<Right>lib/net9.0/Umbraco.Core.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
</Suppressions>
@@ -25,8 +25,8 @@ public class TypeFinderConfig : ITypeFinderConfig
var s = _settings.AssembliesAcceptingLoadExceptions;
return _assembliesAcceptingLoadExceptions = string.IsNullOrWhiteSpace(s)
? Array.Empty<string>()
: s.Split(',').Select(x => x.Trim()).ToArray();
? []
: s.Split(',', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries);
}
}
}
@@ -50,7 +50,7 @@ public class ContentSettings
/// <summary>
/// Gets or sets a value for the collection of error pages.
/// </summary>
public ISet<ContentErrorPage> Error404Collection { get; set; } = new HashSet<ContentErrorPage>();
public IEnumerable<ContentErrorPage> Error404Collection { get; set; } = [];
/// <summary>
/// Gets or sets a value for the preview badge mark-up.
@@ -4,10 +4,23 @@
namespace Umbraco.Cms.Core.Configuration.Models;
/// <summary>
/// The serializer type that nucache uses to persist documents in the database.
/// The serializer type that the published content cache uses to persist documents in the database.
/// </summary>
public enum NuCacheSerializerType
{
MessagePack = 1, // Default
/// <summary>
/// The default serializer type, which uses MessagePack for serialization.
/// </summary>
MessagePack = 1,
/// <summary>
/// The legacy JSON serializer type, which uses JSON for serialization.
/// </summary>
/// <remarks>
/// This option was provided for backward compatibility for the Umbraco cache implementation used from Umbraco 8 to 14 (NuCache).
/// It is no longer supported with the cache implementation from Umbraco 15 based on .NET's Hybrid cache.
/// Use the faster and more compact <see cref="MessagePack"/> instead.
/// The option is kept available only for a more readable format suitable for testing purposes.
/// </remarks>
JSON = 2,
}
+9 -1
View File
@@ -1,4 +1,4 @@
namespace Umbraco.Cms.Core;
namespace Umbraco.Cms.Core;
public static partial class Constants
{
public static class Cache
@@ -9,5 +9,13 @@ public static partial class Constants
public const string Media = "media";
}
/// <summary>
/// Defines the string used to represent a null value in the cache.
/// </summary>
/// <remarks>
/// Used in conjunction with the option to cache null values on the repository caches, so we
/// can distinguish a true null "not found" value and a cached null value.</remarks>
public const string NullRepresentationInCache = "*NULL*";
}
}
+1
View File
@@ -107,6 +107,7 @@ public static partial class Constants
/// <summary>
/// Guid for List View - Members as string
/// </summary>
[Obsolete("No longer used in Umbraco. Scheduled for removal in Umbraco 17.")]
public const string ListViewMembers = "AA2C52A0-CE87-4E65-A47C-7DF09358585D";
/// <summary>

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