Compare commits

...
Author SHA1 Message Date
Nhu DinhandGitHub 9505011d71 Build: Updated nightly E2E test pipeline schedule in v16 (#22805)
Updated nightly E2E test pipeline schedule
2026-05-12 15:36:39 +07:00
Nhu DinhandGitHub 2579aaf2db Build: Cherry pick #22164 for V16 (#22171)
Serialize E2E stages and stagger branch schedules to reduce agent usage
2026-03-19 21:19:45 +07:00
Andy Butland 2b7784a226 Merge branch 'release/16.5.1' into v16/dev 2026-03-10 06:36:50 +01:00
8555a97b39 Merge commit from fork
* Add authorization checks for domain operations.

* Remove duplicate 403 ProducesResponseType attributes.

BackOfficeSecurityRequirementsOperationFilterBase already adds 403
responses for endpoints whose controllers inject IAuthorizationService.

---------

Co-authored-by: Laura Neto <12862535+lauraneto@users.noreply.github.com>
2026-03-10 05:11:16 +01:00
Andy ButlandandGitHub c2dd685a4b Merge commit from fork 2026-03-10 05:10:31 +01:00
Andy ButlandandGitHub 66fc819379 Merge commit from fork
* Protect endpoint that sets user groups for a user collection to prevent elevation of permissions for users.

* Update tests from code review feedback.
2026-03-10 05:07:41 +01:00
Andy Butland 4f1f7e15c4 Bump version to 16.5.1. 2026-02-23 16:38:23 +01:00
Andy Butland a826c52e2e Merge branch 'release/16.5' into v16/dev and bumped version to 16.6.0-rc 2026-01-22 06:45:06 +01:00
Andy Butland 8b2c22aaf1 Merge branch 'release/16.5' of https://github.com/umbraco/Umbraco-CMS into release/16.5 2026-01-21 17:40:17 +01:00
Andy Butland aecfee4469 Bump version to 16.5.0. 2026-01-21 17:39:59 +01:00
Niels LyngsøandGitHub 9c785a9c5b Varying Compositions in Invariant Document Types, Cherrypick of #21267 (#21472)
* cherry picked a5a6d0645f

* correct to getVariesBySegment
2026-01-21 15:27:26 +00:00
Nhu DinhandGitHub 2fe10387ee E2E: V16 QA Update acceptance tests to use refactored UI helpers (#21271)
* Bumped version of test helper

* Bumped version

* Bumped version
2026-01-05 17:25:23 +07:00
8642b9e615 Content Types: Fix property variation change when content exists only in non-default language (#21182)
* Content Types: Fix property variation change when content exists only in non-default language (closes #11771)

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

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

* Add tests for AllowEditInvariantFromNonDefault enabled scenario

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

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

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-17 15:21:17 +01:00
Andreas ZerbstandGitHub 2a604c8719 E2E: QA Replaced unreliable Thread.Sleep(500) with a counter/gate pattern that ensures both transactions are initialized before releasing them to compete for locks (#21165)
Fix flaky test
2025-12-16 12:02:07 +00:00
9c0a0a1086 V16/drag event media (#20893)
* updates the drag event to convert types to lowercase

* clearing up

* clearing up

* Fixed style problem in safari and an unused parameter.

* Fixed lint error.

---------

Co-authored-by: engjlr <enl@umbraco.dk>
Co-authored-by: Engiber Lozada <89547469+engijlr@users.noreply.github.com>
2025-12-15 14:16:57 +00:00
Andy Butland 1a4256f997 Add option to hide colors from icon picker (#20650)
* Add option to hide colors from icon picker

* Hide colors

* Hide colors from config
# Conflicts:
#	src/Umbraco.Web.UI.Client/src/packages/core/icon-registry/icon-picker-modal/icon-picker-modal.element.ts
2025-12-11 06:57:29 +01:00
Andy Butland 80ae0380a2 oEmbed Providers: Updated the X oEmbed provider to use the x.com domain (closes #21052) (#21053)
* Updated the X oEmbed provider to use the x.com domain.

* Fixed issues raised in code review.
2025-12-05 10:55:09 +01:00
Andy Butland fd01282798 Merge branch 'release/16.4.1' into v16/dev
# Conflicts:
#	src/Umbraco.Web.UI.Client/package.json
#	version.json
2025-12-05 06:44:51 +01:00
f7ba2eaa62 Property Editors: Hide "add button" when maximum configuration is 1 (fixes #20407) (#20738)
Hide add button when max 1

Co-authored-by: Lan Nguyen Thuy <lnt@umbraco.dk>
2025-12-04 16:34:10 +01:00
9485a95c0e Image cropper modal import missing component (#20651)
* Import missing component

* Handle nullable type

* Vertically center image

* Add minimum width for SVG without dimensions

* 100% height until max height

* 100% height minus top/bottom padding

* Revert "100% height minus top/bottom padding"

This reverts commit 67ada4c70f4b75dfcfa2b54ce139ec7465a17ce1.

* Revert "Handle nullable type"

This reverts commit 3130e11a4be83a18b5a7d8c1c24ee23c94d8765d.

* Removed flexbox style

* Fixed circular dependency

---------

Co-authored-by: leekelleher <leekelleher@gmail.com>
2025-12-04 16:32:10 +01:00
f1ab605bb9 Debug mode: Marks UMB-DEBUG cookie as HttpOnly and Secure (#21032)
* fix: sets profiling cookie to httpOnly and strict in order to run non-secure

* fix: adds extra message to explain when you can set a cookie

* fix: simplify cookie explanation comment in WebProfilerRepository

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

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

* fix: checks that the profiler is actually enabled and/or disabled and warns the user if that is not the case

* Update src/Umbraco.Web.UI.Client/src/assets/lang/en.ts

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-12-04 10:24:12 +01:00
Andy Butland 3472ff9ba3 Bump version to 16.4.1. 2025-12-03 15:38:10 +01:00
577dc06d55 Delivery API: Only add default strategy if delivery API is not registered. (#20982)
* Only add if not already present

* Update src/Umbraco.Cms.Api.Management/DependencyInjection/WebhooksBuilderExtensions.cs

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

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Kenn Jacobsen <kja@umbraco.dk>
2025-12-03 15:37:03 +01:00
f4771d1495 Delivery API: Only add default strategy if delivery API is not registered. (#20982)
* Only add if not already present

* Update src/Umbraco.Cms.Api.Management/DependencyInjection/WebhooksBuilderExtensions.cs

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

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Kenn Jacobsen <kja@umbraco.dk>
2025-12-03 12:32:30 +01:00
Andy Butland 4b3ce53acf Merge branch 'release/16.4' into v16/dev
# Conflicts:
#	src/Umbraco.Web.UI.Client/package.json
#	version.json
2025-11-26 07:18:09 +01:00
Andy Butland ca267047d3 Bumped version to 16.4.0. 2025-11-25 12:32:28 +01:00
Jacob Overgaard 0543163817 bumps version to 16.5.0-rc 2025-11-24 16:29:11 +01:00
Jacob Overgaard 72f43a5821 Merge branch 'release/16.4' into v16/dev 2025-11-24 16:28:24 +01:00
aea9034adf Localization: Restores region-specific cultures (#20939) (#20942)
* Adds localization manifests for region-specific cultures

This is to support backwards-compatibility and v13 upgradability.

* Removed `uiCulture` from Vietnamese localizations

since it duplicated the English fallback texts.

* 'en' localization file formatting

* Update src/Umbraco.Web.UI.Client/src/assets/lang/en.ts



---------

Co-authored-by: Lee Kelleher <leekelleher@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-11-24 15:27:35 +00:00
Jacob Overgaard 6e6f822761 bumps version to 16.4.0-rc3 2025-11-24 15:24:58 +01:00
Niels LyngsøandGitHub 137aa20a10 Block Editors: avoid discard changes for no changes (Fixes #20680) (#20941)
* ensure Block List only updates if it has an update

* ensures RTE and Grid Block Editor ony updates value if there is a change
2025-11-24 14:04:59 +00:00
Callum WhyteandZeegaan d7231c5435 Preserve existing Examine FieldDefinitionCollection if it already exists (#20931)
* Preserve existing Examine FieldDefinitionCollection if it already exists (#20267)

* Fix missing bracket

* Minor tidy/addition of comments; addition of unit tests.

---------

Co-authored-by: Andy Butland <abutland73@gmail.com>
(cherry picked from commit 908974c6ac)
2025-11-24 12:47:08 +09:00
Andy Butland be116436d9 Migrations: Handles rich text blocks created with TinyMCE in convert local links migration and refreshes internal datatype cache following migration requiring cache rebuild (closes #20885) (#20887)
Handles rich text blocks created with TinyMCE in convert local links migration.
Refreshes internal datatype cache following migration requiring cache rebuild.
# Conflicts:
#	src/Umbraco.Infrastructure/Migrations/MigrationPlanExecutor.cs
2025-11-19 15:35:48 +01:00
Jacob Overgaard aed7505e4b Merge remote-tracking branch 'origin/release/16.4' into v16/dev 2025-11-17 16:39:38 +01:00
590a020303 Redact back-office PKCE codes from the server (V16) (#20851)
Redact back-office PKCE codes from the server (#20847)

* Redact back-office PKCE codes from the server

* Update src/Umbraco.Cms.Api.Common/DependencyInjection/HideBackOfficeTokensHandler.cs

---------

Co-authored-by: Andy Butland <abutland73@gmail.com>
2025-11-17 11:17:18 +01:00
Jacob Overgaard 15c6ca7628 Merge remote-tracking branch 'origin/release/16.4' into v16/dev 2025-11-13 14:15:24 +01:00
49ba89c22a Move access/refresh tokens to secure cookies (#20779)
* feat: adds the `credentials: include` header to all manual requests

* feat: adds `credentials: include` as a configurable option to xhr requests (and sets it by default to true)

* feat: configures the auto-generated fetch client from hey-api to include credentials by default

* Add OpenIddict handler to hide tokens from the back-office client

* Make back-office token redaction optional (default false)

* Clear back-office token cookies on logout

* Add configuration for backoffice cookie settings

* Make cookies forcefully secure + move cookie handler enabling to the BackOfficeTokenCookieSettings

* Use the "__Host-" prefix for cookie names

* docs: adds documentation on cookie settings

* build: sets up launch profile for vscode with new cookie recommended settings

* docs: adds extra note around SameSite settings

* docs: adds extra note around SameSite settings

* Respect sites that do not use HTTPS

* Explicitly invalidate potentially valid, old refresh tokens that should no longer be used

* Removed obsolete const

---------

Co-authored-by: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com>
2025-11-13 08:19:42 +01:00
Andy Butland c295271757 Bumped version to 16.4.0-rc2. 2025-11-13 06:39:10 +01:00
Niels LyngsøandNiels Lyngsø 12b483ff05 Fix block list inline mode (#20745)
* Fix block list inline mode

https://github.com/umbraco/Umbraco-CMS/issues/20618

* Fixed potential runtime errors

* Code cleanup

* Fixed Code Health Review

* Revert some changes

Commented out unused state properties and related code.

* Remove commented-out state property in block workspace view

* fix localization

* no need for question mark after ids, they should be presented as required

---------

Co-authored-by: Niels Lyngsø <niels.lyngso@gmail.com>
Co-authored-by: Niels Lyngsø <nsl@umbraco.dk>
2025-11-10 17:42:16 +01:00
Andy ButlandandGitHub 7502a38033 Dependencies: Update dotnet sdk and node development dependency to latest secure version of current major (16) (#20734)
* Update dotnet sdk and node development dependency to latest secure version of current major.

* Update package-lock.json.
2025-11-05 20:02:11 +01:00
Jacob Overgaard 4e74dbf218 Merge branch 'release/16.4' into v16/dev 2025-11-04 14:07:30 +01:00
Jacob OvergaardandGitHub fa5c53b571 Auth: Cleans up stale or completed auth details from storage (#20725)
* fix: cleans up stale PKCE keys after auth regardless of success or error

* fix: cleans up stale PKCE data on logout
2025-11-04 11:31:34 +00:00
Andreas ZerbstandGitHub 76fed82e91 E2E: QA cherry picked acceptance tests updates from 17 (#20714)
* Updated tests

* Bumped version

* Added v16 to nightly e2e run
2025-11-04 08:23:04 +01:00
Rick ButterfieldandAndy Butland 43ac32282c Preview: Add allow-forms to iframe sandbox attributes (#20701)
Add 'allow-forms' to iframe sandbox attributes
2025-10-31 13:37:35 +01:00
96ecef0a92 Performance: Request cache referenced entities when saving documents with block editors (#20590)
* Added request cache to content and media lookups in mult URL picker.

* Allow property editors to cache referenced entities from block data.

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

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

* Add obsoletions.

* Minor spellcheck

* Ensure request cache is available before relying on it.

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: kjac <kja@umbraco.dk>
2025-10-31 12:41:46 +01:00
Laura Neto 5e87dead44 Task: Dependency track (#20670)
* Generate BOM files on build

* Upload BOM to Dependency Track

* Move Backoffice BOM generation to right after install

The build and/or pack steps are deleting files that are needed for the BOM to be generated properly.

* Split the BOM uploads into different jobs

* Fix wrong usage of parameters

* Move order of dependency track stage

* Fix wrong umbracoVersion value

* Small fixes

* Log curl response headers

* Correct version sent to dependency track

* Adjusted curl flags

* Fix bom file path

* Fix dotnet bom file name

* Add Login UI to dependency track

* Generate BOM for E2E Tests

* Move dependency track stage

* Move acceptance test .env generation to e2e install template

Needed as the post install script is expecting this to exist.

* Use major version if public release

* Missing ')'

* Reverted npm install command changes in static assets project
2025-10-31 12:11:32 +01:00
7af67d2944 Have to control of the state store navigation for custom sections or … (#20637)
* Have to control of the state store navigation for custom sections or overrides

* revert wording

* move logic and update comment

---------

Co-authored-by: Lucas Bach Bisgaard <lucas.bisgaard@kraftvaerk.com>
Co-authored-by: Niels Lyngsø <niels.lyngso@gmail.com>
2025-10-29 20:00:36 +01:00
Jacob Overgaard ce59537006 build: updates lockfile 2025-10-29 11:40:10 +01:00
Jacob Overgaard f87e15b941 build: adds back the ^ missing from openapi-ts to allow newer versions to be used 2025-10-29 11:38:44 +01:00
Andy Butland 1f82bdde3d Merge branch 'release/16.3.4'
# Conflicts:
#	src/Umbraco.Web.UI.Client/package-lock.json
#	src/Umbraco.Web.UI.Client/package.json
#	version.json
2025-10-29 06:45:38 +01:00
0d2393d866 Caching: Resolves publish and install issues related to stale cached data retrieval (closes #20539 and #20630) (#20640)
* Request cache published content creation with version.

* Reload memory cache after install with package migrations.

* Improve message on install for database cache rebuild.

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

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

* Relocated memory cache refresh after package install from notification handler to unattended upgrader.

* Fix construtor breaking change

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: mole <nikolajlauridsen@protonmail.ch>
2025-10-28 13:26:26 +01:00
bea21d7b99 Caching: Resolves publish and install issues related to stale cached data retrieval (closes #20539 and #20630) (#20640)
* Request cache published content creation with version.

* Reload memory cache after install with package migrations.

* Improve message on install for database cache rebuild.

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

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

* Relocated memory cache refresh after package install from notification handler to unattended upgrader.

* Fix construtor breaking change

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: mole <nikolajlauridsen@protonmail.ch>
2025-10-28 12:25:13 +00:00
Andy Butland 3dc65c48b3 Bump package-lock.json to 16.3.4. 2025-10-28 09:58:31 +01:00
Niels LyngsøandAndy Butland 18ab333afc Hotfix: Implement a specific sorting method for statuses as the existing has … (#20609)
Implement a specific sorting method for statuses as the existing has to support deprecated implementation of custom getUnique method
2025-10-28 09:15:17 +01:00
Niels LyngsøandAndy Butland fd91f88a7e Item Repository: Sort statuses by order of unique (#20603)
* utility

* ability to replace

* deprecate removeStatus

* no need to call this any longer

* Sort statuses and ensure not appending statuses, only updating them
# Conflicts:
#	src/Umbraco.Web.UI.Client/src/packages/core/repository/repository-items.manager.ts
2025-10-28 09:15:05 +01:00
Andy Butland 13c164d81f Bump version to 16.3.4. 2025-10-28 08:58:03 +01:00
Andy ButlandandGitHub f33eb3f678 Media types: Handle null configured file extensions when populating allowed media types (closes #20620) (#20635)
* Handle null configured file extensions when populating allowed media types.

* Added clarifying comment.
2025-10-27 13:42:48 +01:00
Jan SkovgaardandSebastiaan Janssen e893682723 Don't call generateAlias on #onAliasChange()
Currently it's not possible to use characters like "_" and "-" in aliases due to this check - At least that is was @nul800sebastiaan told me 😇

Suggested fix for #20622
2025-10-25 10:47:56 +02:00
d9c201e3d1 docs: Add backoffice preview URL to README files (#20623)
* docs: Add backoffice preview URL to README files

Added links to https://backofficepreview.umbraco.com/ in both the main repository README and the Umbraco.Web.UI.Client package README to make the live backoffice preview easily discoverable.

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

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

* docs: fix link

* Update .github/README.md

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Andy Butland <abutland73@gmail.com>
2025-10-24 08:36:34 +02:00
Andy Butland a5fcfc231d Merge branch 'main' of https://github.com/umbraco/Umbraco-CMS 2025-10-24 06:44:08 +02:00
Andy Butland 6ba03a48c8 Merge branch 'release/16.3.3'
# Conflicts:
#	src/Umbraco.Web.UI.Client/package.json
#	version.json
2025-10-24 06:43:55 +02:00
8434c7d0cb Icon Picker: Fix empty selection allowed on mandatory fields and add validation. (#20536)
* Not show the empty tile when filtering is active.

* Added mandatory property to the icon picker.

* Avoid deselecting the icon on second click when not showing the empty option.

* Extends the form control mixin to the icon picker.

* Used super.value.

* Support mandatory from settings config.

* Removed mandatoryConf.

* remove requestUpdate

---------

Co-authored-by: Niels Lyngsø <niels.lyngso@gmail.com>
2025-10-23 13:49:14 +00:00
Bjarne FyrstenborgandGitHub 3854b2bd53 Block List: Remove bold label from inline editing (#20437)
Remove bold label from block list inline editing
2025-10-23 14:32:37 +02:00
dependabot[bot]andJacob Overgaard 08d217360e Bump vite from 7.1.9 to 7.1.11 in /src/Umbraco.Web.UI.Login
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 7.1.9 to 7.1.11.
- [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/v7.1.11/packages/vite)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-23 12:05:29 +02:00
Sebastiaan JanssenandGitHub b762135554 Exclude 'release/no-notes' from release labels 2025-10-23 11:21:25 +02:00
Andy Butland 644334c63b Trees: Restore backward compatibility for file system based tree controllers (closes #20602) (#20608)
* Restore backward compatibility for file system based tree controllers.

* Aligned obsoletion messages.
2025-10-22 16:43:12 +02:00
Andy Butland a09e1777c4 Migrations: Use reliable GUID to check for existence of data type when creating (#20604)
* Use reliable GUID to check for existence of data type in migration.

* Retrieve just a single field in existence check.
2025-10-22 16:43:04 +02:00
Andy Butland 9cb59fe1b4 Bumped version to 16.3.3. 2025-10-22 16:42:12 +02:00
Andy ButlandandGitHub 6bc498ad41 Trees: Restore backward compatibility for file system based tree controllers (closes #20602) (#20608)
* Restore backward compatibility for file system based tree controllers.

* Aligned obsoletion messages.
2025-10-22 14:20:20 +00:00
f88e28d642 Filesystem: Prevent tree showing other filetypes than the supported ones (#20567)
* Added check to only find .css files in FileSystemTreeServiceBase.cs

* Marking GetFiles as virtual and overriding it in StyleSheetTreeService.cs to only find .css files

* Redone tests to fit new format

* Fix tests to use file extensions

* Adding file extensions to all other relevant tests

* Adding file filter to remaining trees

* Adding tests to ensure invalid filetypes wont show

* Encapulation and resolved minor warnings in tests.

---------

Co-authored-by: Andy Butland <abutland73@gmail.com>
2025-10-22 15:12:39 +02:00
Mads RasmussenandNiels Lyngsø 194fee7c91 Use tryExecute for delete API call
Replaces direct await of #delete with tryExecute to improve error handling in the delete method of UmbManagementApiDetailDataRequestManager.
2025-10-22 14:31:41 +02:00
Niels LyngsøandGitHub 4a65f56d9d Hotfix: Implement a specific sorting method for statuses as the existing has … (#20609)
Implement a specific sorting method for statuses as the existing has to support deprecated implementation of custom getUnique method
2025-10-22 11:57:09 +00:00
Sven GeusensandGitHub 62c1d44a5d Webhooks: Register OutputExpansionStrategy for webhooks if Delivery API is not enabled (#20559)
* Register slimmed down OutputExpansionStrategy for webhooks if deliveryapi is not enabled

* PR review comment resolution
2025-10-22 13:46:56 +02:00
Andy Butland c2eea5d6cc Populate IncludeDescendants on ContentPublishedNotification when publishing branch (forward port of #20578). 2025-10-22 13:37:19 +02:00
Niels Lyngsø 21bf23b67d Dictionary: Fix shortcut Ctrl + S not saving dictionary items (#20605)
* switched event listener from 'change' to 'input'

* Update workspace-view-dictionary-editor.element.ts
2025-10-22 12:37:13 +02:00
Andy ButlandandGitHub 48759b9852 Migrations: Use reliable GUID to check for existence of data type when creating (#20604)
* Use reliable GUID to check for existence of data type in migration.

* Retrieve just a single field in existence check.
2025-10-22 10:21:42 +00:00
Niels Lyngsø 79639c0571 Item Repository: Sort statuses by order of unique (#20603)
* utility

* ability to replace

* deprecate removeStatus

* no need to call this any longer

* Sort statuses and ensure not appending statuses, only updating them
# Conflicts:
#	src/Umbraco.Web.UI.Client/src/packages/core/repository/repository-items.manager.ts
2025-10-22 11:52:09 +02:00
Andy Butland 0792e4358b Merge branch 'release/16.3.2'
# Conflicts:
#	src/Umbraco.Web.UI.Client/package.json
#	version.json
2025-10-22 07:11:02 +02:00
Jacob Overgaard caeb3454e1 build(dev): adds umbracoapplicationurl to vscode launch params 2025-10-21 16:22:57 +02:00
Jacob Overgaard 942ccc82d9 docs: Add 'Running Umbraco in Different Modes' section to copilot-instructions 2025-10-21 16:22:36 +02:00
Andy Butland 8aa9dc8f19 Hybrid Cache: Resolve start-up errors with mis-matched types (#20554)
* Be consistent in use of GetOrCreateAsync overload in exists and retrieval.
Ensure nullability of ContentCacheNode is consistent in exists and retrieval.

* Applied suggestion from code review.

* Move seeding to Umbraco application starting rather than started, ensuring an initial request is served.

* Tighten up hybrid cache exists check with locking around check and remove, and use of cancellation token.
2025-10-21 15:27:15 +02:00
Andy Butland 5488c77e0e Bumped version to 16.3.2. 2025-10-21 15:26:29 +02:00
Andy ButlandandGitHub daace4b4a0 Publishing: Resolve exceptions on publish branch (#20464)
* Reduce log level of image cropper converter to avoid flooding logs with expected exceptions.

* Don't run publish branch long running operation on a background thread such that UmbracoContext is available.

* Revert to background thread and use EnsureUmbracoContext to ensure we can get an IUmbracoContext in the URL providers.

* Updated tests.

* Applied suggestion from code review.

* Clarified comment.
2025-10-21 12:05:10 +02:00
Andy ButlandandGitHub 1ceec183a3 Media: Fixes SQL error to ensure database relation between user group media start folder and deleted media item is removed (closes #20555) (#20572)
Fixes SQL error to ensure database relation between user group media start folder and deleted media item is removed.
2025-10-21 11:38:01 +02:00
Andy ButlandandGitHub 81a8a0c191 Hybrid Cache: Resolve start-up errors with mis-matched types (#20554)
* Be consistent in use of GetOrCreateAsync overload in exists and retrieval.
Ensure nullability of ContentCacheNode is consistent in exists and retrieval.

* Applied suggestion from code review.

* Move seeding to Umbraco application starting rather than started, ensuring an initial request is served.

* Tighten up hybrid cache exists check with locking around check and remove, and use of cancellation token.
2025-10-21 09:57:29 +02:00
ae41438a36 Tiptap RTE: Allow removal of unregistered extensions (#20571)
* Tiptap toolbar config: enable removal of unregistered extensions

* Tiptap statusbar config: enable removal of unregistered extensions

* Tiptap toolbar config: Typescript tidy-up

* Tiptap toolbar sorting amend

Removed the need for the `tiptap-toolbar-alias` attribute,
we can reuse the `data-mark`.

* Tiptap extension config UI amend

If the extension doesn't have a `description`,
then add the `alias` to the title/tooltip, to give a DX hint.

* Tiptap toolbar: adds `title` to placeholder skeleton

* Added missing `forExtensions` for Style Select and Horizontal Rule toolbar extensions

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

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

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

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

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-10-21 07:28:01 +00:00
dependabot[bot]andJacob Overgaard 5337c38f2c Bump vite from 7.1.9 to 7.1.11 in /src/Umbraco.Web.UI.Client
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 7.1.9 to 7.1.11.
- [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/v7.1.11/packages/vite)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-21 09:13:34 +02:00
Nhu DinhandGitHub 7751e40ba8 E2E: QA Fixed the flaky tests related to publishing content with image cropper (#20577)
Added more waits
2025-10-21 08:50:13 +02:00
Lee KelleherandGitHub d5a2f0572e Preview: Redirect to published URL on exit (#20556)
* Preview Exit: Gets the page's published URL on exit for redirect

* Preview Open Website: Uses the page's published URL

* Tweaked the published URL logic

* Code amends based on @copilot's suggestions
2025-10-20 11:51:38 +02:00
Andy Butland 5a65eb1758 Update OpenApi.json and client-side models. 2025-10-17 15:01:09 +02:00
Anders ReusandGitHub 105cb9da41 Added trashed state so when requesting content from the recycle bin via the management api it will return trashed instead of published state (#20542)
Added trashed state so when requesting content from the recycle bin via the management api, the state will be trashed instead of published.
2025-10-17 14:40:18 +02:00
Andy ButlandandJacob Overgaard a3a8be4717 Templates: Retain layout from file when loading template (closes #20524) (#20529)
Retain layout from file when loading template.
2025-10-17 10:47:21 +02:00
Jacob Overgaard d17ba805b2 build(deps): bumps @umbraco-ui/uui from 1.16.0-rc.0 to 1.16.0 2025-10-17 09:56:42 +02:00
Andy Butland 96f597e440 Merge branch 'release/16.3.1'
# Conflicts:
#	src/Umbraco.Web.UI.Client/package.json
#	version.json
2025-10-17 09:55:20 +02:00
Ben WhiteandAndy Butland 6458bb40f9 Don't use non-generic ILogger as a fallback in BlockEditorPropertyValueEditor (#20532)
Update logger service retrieval in BlockEditorPropertyValueEditor
2025-10-16 21:07:16 +02:00
Ben WhiteandGitHub 31bcbc1147 Don't use non-generic ILogger as a fallback in BlockEditorPropertyValueEditor (#20532)
Update logger service retrieval in BlockEditorPropertyValueEditor
2025-10-16 21:06:10 +02:00
Andy Butland 62edad17a1 Bumped version to 16.3.1. 2025-10-16 20:50:59 +02:00
188 changed files with 4654 additions and 1789 deletions
+10 -3
View File
@@ -37,7 +37,7 @@ In order to work with the Umbraco source code locally, first make sure you have
### Familiarizing yourself with the code
Umbraco is a .NET application using C#. The solution is broken down into multiple projects. There are several class libraries. The `Umbraco.Web.UI` project is the main project that hosts the back office and login screen. This is the project you will want to run to see your changes.
Umbraco is a .NET application using C#. The solution is broken down into multiple projects. There are several class libraries. The `Umbraco.Web.UI` project is the main project that hosts the back office and login screen. This is the project you will want to run to see your changes.
There are two web projects in the solution with client-side assets based on TypeScript, `Umbraco.Web.UI.Client` and `Umbraco.Web.UI.Login`.
@@ -73,13 +73,20 @@ Just be careful not to include this change in your PR.
Conversely, if you are working on front-end only, you want to build the back-end once and then run it. Before you do so, update the configuration in `appSettings.json` to add the following under `Umbraco:Cms:Security`:
```
```json
"BackOfficeHost": "http://localhost:5173",
"AuthorizeCallbackPathName": "/oauth_complete",
"AuthorizeCallbackLogoutPathName": "/logout",
"AuthorizeCallbackErrorPathName": "/error"
"AuthorizeCallbackErrorPathName": "/error",
"BackOfficeTokenCookie": {
"Enabled": true,
"SameSite": "None"
}
```
> [!NOTE]
> If you get stuck in a login loop, try clearing your browser cookies for localhost, and make sure that the `BackOfficeTokenCookie` settings are correct. Namely, that `SameSite` should be set to `None` when running the front-end server separately.
Then run Umbraco from the command line.
```
+8
View File
@@ -38,6 +38,14 @@ Some important documentation links to get you started:
- [Getting to know Umbraco](https://docs.umbraco.com/umbraco-cms/fundamentals/get-to-know-umbraco)
- [Tutorials for creating a basic website and customizing the editing experience](https://docs.umbraco.com/umbraco-cms/tutorials/overview)
## Backoffice Preview
Want to see the latest backoffice UI in action? Check out our live preview:
**[backofficepreview.umbraco.com](https://backofficepreview.umbraco.com/)**
This preview is automatically deployed from the main branch and showcases the latest backoffice features and improvements. It runs from mock data and persistent edits are not supported.
## Get help
If you need a bit of feedback while building your Umbraco projects, we are [chatty on Discord](https://discord.umbraco.com). Our Discord server serves as a social space for all Umbracians. If you have any questions or need some help with a problem, head over to our [dedicated forum](https://forum.umbraco.com/) where the Umbraco Community will be happy to help.
+21 -6
View File
@@ -94,19 +94,34 @@ The solution contains 30 C# projects organized as follows:
## Common Tasks
### Frontend Development
For frontend-only changes:
1. Configure backend for frontend development:
```json
<!-- Add to src/Umbraco.Web.UI/appsettings.json under Umbraco:Cms:Security: -->
### Running Umbraco in Different Modes
**Production Mode (Standard Development)**
Use this for backend development, testing full builds, or when you don't need hot reloading:
1. Build frontend assets: `cd src/Umbraco.Web.UI.Client && npm run build:for:cms`
2. Run backend: `cd src/Umbraco.Web.UI && dotnet run --no-build`
3. Access backoffice: `https://localhost:44339/umbraco`
4. Application uses compiled frontend from `wwwroot/umbraco/backoffice/`
**Vite Dev Server Mode (Frontend Development with Hot Reload)**
Use this for frontend-only development with hot module reloading:
1. Configure backend for frontend development - Add to `src/Umbraco.Web.UI/appsettings.json` under `Umbraco:CMS:Security`:
```json
"BackOfficeHost": "http://localhost:5173",
"AuthorizeCallbackPathName": "/oauth_complete",
"AuthorizeCallbackLogoutPathName": "/logout",
"AuthorizeCallbackErrorPathName": "/error"
"AuthorizeCallbackErrorPathName": "/error",
"BackOfficeTokenCookie": {
"Enabled": true,
"SameSite": "None"
}
```
2. Run backend: `cd src/Umbraco.Web.UI && dotnet run --no-build`
3. Run frontend dev server: `cd src/Umbraco.Web.UI.Client && npm run dev:server`
4. Access backoffice: `http://localhost:5173/` (no `/umbraco` prefix)
5. Changes to TypeScript/Lit files hot reload automatically
**Important:** Remove the `BackOfficeHost` configuration before committing or switching back to production mode.
### Backend-Only Development
For backend-only changes, disable frontend builds:
@@ -52,7 +52,7 @@ jobs:
for (const item of items) {
const releaseLabels = (item.labels || [])
.map(l => (typeof l === "string" ? l : l.name)) // always get the name
.filter(n => typeof n === "string" && n.startsWith("release/"));
.filter(n => typeof n === "string" && n.startsWith("release/") && n !== "release/no-notes");
if (releaseLabels.length === 0) continue;
core.info(`#${item.number}: ${releaseLabels.join(", ")}`);
+5 -1
View File
@@ -101,10 +101,14 @@
"env": {
"ASPNETCORE_ENVIRONMENT": "Development",
"ASPNETCORE_URLS": "https://localhost:44339",
"UMBRACO__CMS__WEBROUTING__UMBRACOAPPLICATIONURL": "https://localhost:44339",
"UMBRACO__CMS__SECURITY__BACKOFFICEHOST": "http://localhost:5173",
"UMBRACO__CMS__SECURITY__AUTHORIZECALLBACKPATHNAME": "/oauth_complete",
"UMBRACO__CMS__SECURITY__AUTHORIZECALLBACKLOGOUTPATHNAME": "/logout",
"UMBRACO__CMS__SECURITY__AUTHORIZECALLBACKERRORPATHNAME": "/error"
"UMBRACO__CMS__SECURITY__AUTHORIZECALLBACKERRORPATHNAME": "/error",
"UMBRACO__CMS__SECURITY__KEEPUSERLOGGEDIN": "true",
"UMBRACO__CMS__SECURITY__BACKOFFICETOKENCOOKIE__ENABLED": "true",
"UMBRACO__CMS__SECURITY__BACKOFFICETOKENCOOKIE__SAMESITE": "None"
},
"sourceFileMap": {
"/Views": "${workspaceFolder}/Umbraco.Web.UI/Views"
+81
View File
@@ -34,6 +34,10 @@ parameters:
displayName: Upload API docs
type: boolean
default: false
- name: uploadDependencyTrack
displayName: Upload BOMs to Dependency Track
type: boolean
default: false
- name: forceReleaseTestFilter
displayName: Force to use the release test filters
type: boolean
@@ -103,6 +107,15 @@ stages:
command: build
projects: $(solution)
arguments: "--configuration $(buildConfiguration) --no-restore --property:ContinuousIntegrationBuild=true --property:GeneratePackageOnBuild=true --property:PackageOutputPath=$(Build.ArtifactStagingDirectory)/nupkg"
- powershell: |
dotnet tool install --global CycloneDX
dotnet-CycloneDX $(solution) --output $(Build.ArtifactStagingDirectory)/bom --filename bom-dotnet.xml
displayName: 'Generate Backend BOM'
- powershell: |
npm install --global @cyclonedx/cyclonedx-npm
cyclonedx-npm -o $(Build.ArtifactStagingDirectory)\bom\bom-login.xml --ignore-npm-errors --verbose
displayName: Generate Login UI BOM
workingDirectory: src/Umbraco.Web.UI.Login
- task: PublishPipelineArtifact@1
displayName: Publish nupkg
inputs:
@@ -113,6 +126,11 @@ stages:
inputs:
targetPath: $(Build.SourcesDirectory)
artifactName: build_output
- task: PublishPipelineArtifact@1
displayName: Publish Backend BOM
inputs:
targetPath: $(Build.ArtifactStagingDirectory)/bom
artifactName: bom-backend
- job: B
displayName: Build Bellissima Package
@@ -124,6 +142,11 @@ stages:
lfs: false,
fetchDepth: 500
- template: templates/backoffice-install.yml
- powershell: |
npm install --global @cyclonedx/cyclonedx-npm
cyclonedx-npm -o $(Build.ArtifactStagingDirectory)/bom/bom-backoffice.xml --ignore-npm-errors --verbose
displayName: Generate Backoffice UI BOM
workingDirectory: src/Umbraco.Web.UI.Client
- script: npm run build:for:npm
displayName: Run build:for:npm
workingDirectory: src/Umbraco.Web.UI.Client
@@ -140,6 +163,35 @@ stages:
inputs:
targetPath: $(Build.ArtifactStagingDirectory)/npm
artifactName: npm
- publish: $(Build.ArtifactStagingDirectory)/bom
artifact: bom-frontend
displayName: 'Publish Frontend BOM'
- stage: E2E_BOM
displayName: E2E Tests BOM Generation
dependsOn: []
jobs:
- job:
displayName: E2E Generate BOM
pool:
vmImage: "ubuntu-latest"
steps:
- checkout: self
submodules: false
lfs: false,
fetchDepth: 500
- template: templates/e2e-install.yml
parameters:
nodeVersion: ${{ variables.nodeVersion }}
npm_config_cache: ${{ variables.npm_config_cache }}
- powershell: |
npm install --global @cyclonedx/cyclonedx-npm
cyclonedx-npm -o $(Build.ArtifactStagingDirectory)/bom/bom-e2e.xml --ignore-npm-errors --verbose
displayName: Generate E2E Tests BOM
workingDirectory: tests/Umbraco.Tests.AcceptanceTest
- publish: $(Build.ArtifactStagingDirectory)/bom
artifact: bom-e2e
displayName: 'Publish E2E BOM'
- stage: Build_Docs
condition: and(succeeded(), or(eq(dependencies.Build.outputs['A.build.NBGV_PublicRelease'], 'True'), ${{parameters.buildApiDocs}}))
@@ -668,6 +720,34 @@ stages:
ASPNETCORE_URLS: ${{ variables.ASPNETCORE_URLS }}
DatabaseType: ${{ variables.DatabaseType }}
- stage: Dependency_Track
displayName: Dependency Track
dependsOn:
- Build
- E2E_BOM
condition: and(succeeded(), or(eq(dependencies.Build.outputs['A.build.NBGV_PublicRelease'], 'True'), ${{parameters.uploadDependencyTrack}}))
variables:
# Determine Umbraco version based on whether it's a public release or not. If public release, use major version, else use full NuGet package version.
umbracoVersion: $[ iif(eq(stageDependencies.Build.A.outputs['build.NBGV_PublicRelease'], 'True'), stageDependencies.Build.A.outputs['build.NBGV_VersionMajor'], stageDependencies.Build.A.outputs['build.NBGV_NuGetPackageVersion']) ]
jobs:
- template: templates/dependency-track.yml
parameters:
projectName: "Umbraco-CMS"
umbracoVersion: $(umbracoVersion)
projects:
- name: "Backend"
artifact: "bom-backend"
bomFilePath: "bom-dotnet.xml"
- name: "Login"
artifact: "bom-backend"
bomFilePath: "bom-login.xml"
- name: "Backoffice"
artifact: "bom-frontend"
bomFilePath: "bom-backoffice.xml"
- name: "E2E"
artifact: "bom-e2e"
bomFilePath: "bom-e2e.xml"
###############################################
## Release
###############################################
@@ -874,3 +954,4 @@ stages:
ContainerName: "$web"
BlobPrefix: v$(umbracoMajorVersion)/ui-api
CleanTargetBeforeCopy: true
+7 -27
View File
@@ -26,38 +26,18 @@ steps:
artifact: nupkg
path: $(Agent.BuildDirectory)/app/nupkg
- task: NodeTool@0
displayName: Use Node.js $(nodeVersion)
inputs:
versionSpec: $(nodeVersion)
- task: UseDotNet@2
displayName: Use .NET SDK from global.json
inputs:
useGlobalJson: true
- pwsh: |
"UMBRACO_USER_LOGIN=${{ parameters.PlaywrightUserEmail }}
UMBRACO_USER_PASSWORD=${{ parameters.PlaywrightPassword }}
URL=${{ parameters.ASPNETCORE_URLS }}
STORAGE_STAGE_PATH=$(Build.SourcesDirectory)/tests/Umbraco.Tests.AcceptanceTest/playwright/.auth/user.json
CONSOLE_ERRORS_PATH=$(Build.SourcesDirectory)/tests/Umbraco.Tests.AcceptanceTest/console-errors.json" | Out-File .env
displayName: Generate .env
workingDirectory: $(Build.SourcesDirectory)/tests/Umbraco.Tests.AcceptanceTest
# Cache and restore NPM packages
- task: Cache@2
displayName: Cache NPM packages
inputs:
key: 'npm_e2e | "$(Agent.OS)" | $(Build.SourcesDirectory)/tests/Umbraco.Tests.AcceptanceTest/package-lock.json'
restoreKeys: |
npm_e2e | "$(Agent.OS)"
npm_e2e
path: ${{ parameters.npm_config_cache }}
- script: npm ci --no-fund --no-audit --prefer-offline
workingDirectory: $(Build.SourcesDirectory)/tests/Umbraco.Tests.AcceptanceTest
displayName: Restore NPM packages
- template: templates/e2e-install.yml
parameters:
nodeVersion: ${{ parameters.nodeVersion }}
npm_config_cache: ${{ parameters.npm_config_cache }}
PlaywrightUserEmail: ${{ parameters.PlaywrightUserEmail }}
PlaywrightPassword: ${{ parameters.PlaywrightPassword }}
ASPNETCORE_URLS: ${{ parameters.ASPNETCORE_URLS }}
# Install Template
- pwsh: |
+8 -7
View File
@@ -4,12 +4,11 @@ pr: none
trigger: none
schedules:
- cron: '0 0 * * *'
displayName: Daily midnight build
- cron: '0 6 * * *'
displayName: Daily 6 AM build (v16/dev)
branches:
include:
- v15/dev
- main
- v16/dev
parameters:
- name: skipIntegrationTests
@@ -294,7 +293,8 @@ stages:
- stage: DefaultConfigE2E
displayName: Default Config E2E Tests
dependsOn: Build
dependsOn: Integration
condition: always()
variables:
npm_config_cache: $(Pipeline.Workspace)/.npm_e2e
# Enable console logging in Release mode
@@ -475,7 +475,8 @@ stages:
- stage: AdditionalConfigE2E
displayName: Additional Config E2E Tests
dependsOn: Build
dependsOn: DefaultConfigE2E
condition: always()
variables:
npm_config_cache: $(Pipeline.Workspace)/.npm_e2e
ASPNETCORE_URLS: https://localhost:44331
@@ -674,4 +675,4 @@ stages:
--data "$PAYLOAD" \
"$SLACK_WEBHOOK_URL"
env:
SLACK_WEBHOOK_URL: $(E2ESLACKWEBHOOKURL)
SLACK_WEBHOOK_URL: $(E2ESLACKWEBHOOKURL)
+56
View File
@@ -0,0 +1,56 @@
parameters:
- name: projectName
type: string
- name: umbracoVersion
type: string
- name: projects
type: object
jobs:
- job: Create_DT_Project
displayName: Create Dependency Track Project
steps:
- checkout: none
- bash: |
project_id=$(curl --no-progress-meter -H "X-Api-Key: $(DT_API_KEY)" "$(DT_API_URL)/v1/project/lookup?name=${{ parameters.projectName }}&version=${{ parameters.umbracoVersion }}" | jq -r '.uuid')
if [ "$project_id" != "null" ] && [ -n "$project_id" ]; then
echo "Project '${{ parameters.projectName }}' with version '${{ parameters.umbracoVersion }}' already exists (ID: $project_id)."
else
project_id=$(curl --no-progress-meter \
-X PUT "$(DT_API_URL)/v1/project" \
-H "X-Api-Key: $(DT_API_KEY)" \
-H "Content-Type: application/json" \
-d '{"name": "${{ parameters.projectName }}", "version": "${{ parameters.umbracoVersion }}", "collectionLogic": "AGGREGATE_DIRECT_CHILDREN"}' \
| jq -r '.uuid')
if [ -z "$project_id" ] || [ "$project_id" == "null" ]; then
echo "Failed to create project '${{ parameters.projectName }}' version '${{ parameters.umbracoVersion }}'."
exit 1
fi
echo "Created project '${{ parameters.projectName }}' with version '${{ parameters.umbracoVersion }}' (ID: $project_id)."
fi
displayName: Ensure main project exists in Dependency Track
- ${{ each project in parameters.projects }}:
- job:
displayName: Upload ${{ project.name }} BOM
dependsOn: Create_DT_Project
steps:
- checkout: none
- download: current
artifact: ${{ project.artifact }}
displayName: Download ${{ project.artifact }} artifact
- script: |
curl --no-progress-meter --fail-with-body \
-X POST "$(DT_API_URL)/v1/bom" \
-H "X-Api-Key: $(DT_API_KEY)" \
-H "Content-Type: multipart/form-data" \
-F "autoCreate=true" \
-F "projectName=${{ parameters.projectName }}-${{ project.name }}" \
-F "projectVersion=${{ parameters.umbracoVersion }}" \
-F "parentName=${{ parameters.projectName }}" \
-F "parentVersion=${{ parameters.umbracoVersion }}" \
-F "bom=@$(Pipeline.Workspace)/${{ project.artifact }}/${{ project.bomFilePath }}"
displayName: Upload ${{ project.name }} BOM to Dependency Track
+49
View File
@@ -0,0 +1,49 @@
parameters:
- name: nodeVersion
type: string
default: ''
- name: npm_config_cache
type: string
default: ''
- name: PlaywrightUserEmail
type: string
default: ''
- name: PlaywrightPassword
type: string
default: ''
- name: ASPNETCORE_URLS
type: string
default: ''
steps:
- task: NodeTool@0
displayName: Use Node.js $(nodeVersion)
inputs:
versionSpec: $(nodeVersion)
- pwsh: |
"UMBRACO_USER_LOGIN=${{ parameters.PlaywrightUserEmail }}
UMBRACO_USER_PASSWORD=${{ parameters.PlaywrightPassword }}
URL=${{ parameters.ASPNETCORE_URLS }}
STORAGE_STAGE_PATH=$(Build.SourcesDirectory)/tests/Umbraco.Tests.AcceptanceTest/playwright/.auth/user.json
CONSOLE_ERRORS_PATH=$(Build.SourcesDirectory)/tests/Umbraco.Tests.AcceptanceTest/console-errors.json" | Out-File .env
displayName: Generate .env
workingDirectory: $(Build.SourcesDirectory)/tests/Umbraco.Tests.AcceptanceTest
# Cache and restore NPM packages
- task: Cache@2
displayName: Cache NPM packages
inputs:
key: 'npm_e2e | "$(Agent.OS)" | $(Build.SourcesDirectory)/tests/Umbraco.Tests.AcceptanceTest/package-lock.json'
restoreKeys: |
npm_e2e | "$(Agent.OS)"
npm_e2e
path: ${{ parameters.npm_config_cache }}
- script: npm ci --no-fund --no-audit --prefer-offline
workingDirectory: $(Build.SourcesDirectory)/tests/Umbraco.Tests.AcceptanceTest
displayName: Restore NPM packages
+1 -1
View File
@@ -1,6 +1,6 @@
{
"sdk": {
"version": "9.0.100",
"version": "9.0.306",
"rollForward": "latestFeature",
"allowPrerelease": false
}
@@ -0,0 +1,12 @@
using Microsoft.AspNetCore.Http;
using Umbraco.Cms.Core.DeliveryApi;
namespace Umbraco.Cms.Api.Common.Accessors;
public sealed class RequestContextOutputExpansionStrategyAccessor : RequestContextServiceAccessorBase<IOutputExpansionStrategy>, IOutputExpansionStrategyAccessor
{
public RequestContextOutputExpansionStrategyAccessor(IHttpContextAccessor httpContextAccessor)
: base(httpContextAccessor)
{
}
}
@@ -0,0 +1,20 @@
using System.Diagnostics.CodeAnalysis;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
namespace Umbraco.Cms.Api.Common.Accessors;
public abstract class RequestContextServiceAccessorBase<T>
where T : class
{
private readonly IHttpContextAccessor _httpContextAccessor;
protected RequestContextServiceAccessorBase(IHttpContextAccessor httpContextAccessor)
=> _httpContextAccessor = httpContextAccessor;
public bool TryGetValue([NotNullWhen(true)] out T? requestStartNodeService)
{
requestStartNodeService = _httpContextAccessor.HttpContext?.RequestServices.GetService<T>();
return requestStartNodeService is not null;
}
}
@@ -0,0 +1,230 @@
using System.Diagnostics.CodeAnalysis;
using Microsoft.AspNetCore.DataProtection;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Options;
using OpenIddict.Server;
using OpenIddict.Validation;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Configuration.Models;
using Umbraco.Cms.Core.Events;
using Umbraco.Cms.Core.Notifications;
using Umbraco.Cms.Web.Common.Security;
using Umbraco.Extensions;
namespace Umbraco.Cms.Api.Common.DependencyInjection;
internal sealed class HideBackOfficeTokensHandler
: IOpenIddictServerHandler<OpenIddictServerEvents.ApplyTokenResponseContext>,
IOpenIddictServerHandler<OpenIddictServerEvents.ApplyAuthorizationResponseContext>,
IOpenIddictServerHandler<OpenIddictServerEvents.ExtractTokenRequestContext>,
IOpenIddictValidationHandler<OpenIddictValidationEvents.ProcessAuthenticationContext>,
INotificationHandler<UserLogoutSuccessNotification>
{
private const string RedactedTokenValue = "[redacted]";
private const string AccessTokenCookieKey = "__Host-umbAccessToken";
private const string RefreshTokenCookieKey = "__Host-umbRefreshToken";
private const string PkceCodeCookieKey = "__Host-umbPkceCode";
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly IDataProtectionProvider _dataProtectionProvider;
private readonly BackOfficeTokenCookieSettings _backOfficeTokenCookieSettings;
private readonly GlobalSettings _globalSettings;
public HideBackOfficeTokensHandler(
IHttpContextAccessor httpContextAccessor,
IDataProtectionProvider dataProtectionProvider,
IOptions<BackOfficeTokenCookieSettings> backOfficeTokenCookieSettings,
IOptions<GlobalSettings> globalSettings)
{
_httpContextAccessor = httpContextAccessor;
_dataProtectionProvider = dataProtectionProvider;
_backOfficeTokenCookieSettings = backOfficeTokenCookieSettings.Value;
_globalSettings = globalSettings.Value;
}
/// <summary>
/// This is invoked when tokens (access and refresh tokens) are issued to a client. For the back-office client,
/// we will intercept the response, write the tokens from the response into HTTP-only cookies, and redact the
/// tokens from the response, so they are not exposed to the client.
/// </summary>
public ValueTask HandleAsync(OpenIddictServerEvents.ApplyTokenResponseContext context)
{
if (context.Request?.ClientId is not Constants.OAuthClientIds.BackOffice)
{
// Only ever handle the back-office client.
return ValueTask.CompletedTask;
}
HttpContext httpContext = GetHttpContext();
if (context.Response.AccessToken is not null)
{
SetCookie(httpContext, AccessTokenCookieKey, context.Response.AccessToken);
context.Response.AccessToken = RedactedTokenValue;
}
if (context.Response.RefreshToken is not null)
{
SetCookie(httpContext, RefreshTokenCookieKey, context.Response.RefreshToken);
context.Response.RefreshToken = RedactedTokenValue;
}
return ValueTask.CompletedTask;
}
/// <summary>
/// This is invoked when a PKCE code is issued to the client. For the back-office client, we will intercept the
/// response, write the PKCE code from the response into a HTTP-only cookie, and redact the code from the response,
/// so it's not exposed to the client.
/// </summary>
public ValueTask HandleAsync(OpenIddictServerEvents.ApplyAuthorizationResponseContext context)
{
if (context.Request?.ClientId is not Constants.OAuthClientIds.BackOffice)
{
// Only ever handle the back-office client.
return ValueTask.CompletedTask;
}
if (context.Response.Code is not null)
{
SetCookie(GetHttpContext(), PkceCodeCookieKey, context.Response.Code);
context.Response.Code = RedactedTokenValue;
}
return ValueTask.CompletedTask;
}
/// <summary>
/// This is invoked when requesting new tokens.
/// </summary>
public ValueTask HandleAsync(OpenIddictServerEvents.ExtractTokenRequestContext context)
{
if (context.Request?.ClientId != Constants.OAuthClientIds.BackOffice)
{
// Only ever handle the back-office client.
return ValueTask.CompletedTask;
}
// Handle when the PKCE code is being exchanged for an access token.
if (context.Request.Code == RedactedTokenValue
&& TryGetCookie(PkceCodeCookieKey, out var code))
{
context.Request.Code = code;
// We won't need the PKCE cookie after this, let's remove it.
RemoveCookie(GetHttpContext(), PkceCodeCookieKey);
}
else
{
// PCKE codes should always be redacted. If we got here, someone might be trying to pass another PKCE
// code. For security reasons, explicitly discard the code (if any) to be on the safe side.
context.Request.Code = null;
}
// Handle when a refresh token is being exchanged for a new access token.
if (context.Request.RefreshToken == RedactedTokenValue
&& TryGetCookie(RefreshTokenCookieKey, out var refreshToken))
{
context.Request.RefreshToken = refreshToken;
}
else
{
// If we got here, either the refresh token was not redacted, or nothing was found in the refresh token cookie.
// If OpenIddict found a refresh token, it could be an old token that is potentially still valid. For security
// reasons, we cannot accept that; at this point, we expect the refresh tokens to be explicitly redacted.
context.Request.RefreshToken = null;
}
return ValueTask.CompletedTask;
}
/// <summary>
/// This is invoked when extracting the auth context for a client request.
/// </summary>
public ValueTask HandleAsync(OpenIddictValidationEvents.ProcessAuthenticationContext context)
{
// For the back-office client, this only happens when an access token is sent to the API.
if (context.AccessToken != RedactedTokenValue)
{
return ValueTask.CompletedTask;
}
if (TryGetCookie(AccessTokenCookieKey, out var accessToken))
{
context.AccessToken = accessToken;
}
return ValueTask.CompletedTask;
}
public void Handle(UserLogoutSuccessNotification notification)
{
HttpContext? context = _httpContextAccessor.HttpContext;
if (context is null)
{
// For some reason there is no ambient HTTP context, so we can't clean up the cookies.
// This is OK, because the tokens in the cookies have already been revoked at user sign-out,
// so the cookie clean-up is mostly cosmetic.
return;
}
context.Response.Cookies.Delete(AccessTokenCookieKey);
context.Response.Cookies.Delete(RefreshTokenCookieKey);
}
private HttpContext GetHttpContext()
=> _httpContextAccessor.GetRequiredHttpContext();
private void SetCookie(HttpContext httpContext, string key, string value)
{
var cookieValue = EncryptionHelper.Encrypt(value, _dataProtectionProvider);
RemoveCookie(httpContext, key);
httpContext.Response.Cookies.Append(key, cookieValue, GetCookieOptions(httpContext));
}
private void RemoveCookie(HttpContext httpContext, string key)
=> httpContext.Response.Cookies.Delete(key, GetCookieOptions(httpContext));
private CookieOptions GetCookieOptions(HttpContext httpContext) =>
new()
{
// Prevent the client-side scripts from accessing the cookie.
HttpOnly = true,
// Mark the cookie as essential to the application, to enforce it despite any
// data collection consent options. This aligns with how ASP.NET Core Identity
// does when writing cookies for cookie authentication.
IsEssential = true,
// Cookie path must be root for optimal security.
Path = "/",
// For optimal security, the cooke must be secure. However, Umbraco allows for running development
// environments over HTTP, so we need to take that into account here.
// Thus, we will make the cookie secure if:
// - HTTPS is explicitly enabled by config (default for production environments), or
// - The current request is over HTTPS (meaning the environment supports it regardless of config).
Secure = _globalSettings.UseHttps || httpContext.Request.IsHttps,
// SameSite is configurable (see BackOfficeTokenCookieSettings for defaults):
SameSite = ParseSameSiteMode(_backOfficeTokenCookieSettings.SameSite),
};
private bool TryGetCookie(string key, [NotNullWhen(true)] out string? value)
{
if (GetHttpContext().Request.Cookies.TryGetValue(key, out var cookieValue))
{
value = EncryptionHelper.Decrypt(cookieValue, _dataProtectionProvider);
return true;
}
value = null;
return false;
}
private static SameSiteMode ParseSameSiteMode(string sameSiteMode) =>
Enum.TryParse(sameSiteMode, ignoreCase: true, out SameSiteMode result)
? result
: throw new ArgumentException($"The provided {nameof(sameSiteMode)} value could not be parsed into as SameSiteMode value.", nameof(sameSiteMode));
}
@@ -9,6 +9,7 @@ using Umbraco.Cms.Api.Common.Security;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Configuration.Models;
using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Core.Notifications;
using Umbraco.Cms.Infrastructure.BackgroundJobs.Jobs;
using Umbraco.Extensions;
@@ -28,6 +29,11 @@ public static class UmbracoBuilderAuthExtensions
private static void ConfigureOpenIddict(IUmbracoBuilder builder)
{
// Optionally hide tokens from the back-office.
var hideBackOfficeTokens = (builder.Config
.GetSection(Constants.Configuration.ConfigBackOfficeTokenCookie)
.Get<BackOfficeTokenCookieSettings>() ?? new BackOfficeTokenCookieSettings()).Enabled;
builder.Services.AddOpenIddict()
// Register the OpenIddict server components.
.AddServer(options =>
@@ -113,6 +119,28 @@ public static class UmbracoBuilderAuthExtensions
{
configuration.UseSingletonHandler<ProcessRequestContextHandler>().SetOrder(OpenIddict.Server.AspNetCore.OpenIddictServerAspNetCoreHandlers.ResolveRequestUri.Descriptor.Order - 1);
});
if (hideBackOfficeTokens)
{
options.AddEventHandler<OpenIddictServerEvents.ApplyTokenResponseContext>(configuration =>
{
configuration
.UseSingletonHandler<HideBackOfficeTokensHandler>()
.SetOrder(OpenIddict.Server.AspNetCore.OpenIddictServerAspNetCoreHandlers.ProcessJsonResponse<OpenIddictServerEvents.ApplyTokenResponseContext>.Descriptor.Order - 1);
});
options.AddEventHandler<OpenIddictServerEvents.ApplyAuthorizationResponseContext>(configuration =>
{
configuration
.UseSingletonHandler<HideBackOfficeTokensHandler>()
.SetOrder(OpenIddict.Server.AspNetCore.OpenIddictServerAspNetCoreHandlers.Authentication.ProcessQueryResponse.Descriptor.Order - 1);
});
options.AddEventHandler<OpenIddictServerEvents.ExtractTokenRequestContext>(configuration =>
{
configuration
.UseSingletonHandler<HideBackOfficeTokensHandler>()
.SetOrder(OpenIddict.Server.AspNetCore.OpenIddictServerAspNetCoreHandlers.ExtractPostRequest<OpenIddictServerEvents.ExtractTokenRequestContext>.Descriptor.Order + 1);
});
}
})
// Register the OpenIddict validation components.
@@ -137,9 +165,25 @@ public static class UmbracoBuilderAuthExtensions
{
configuration.UseSingletonHandler<ProcessRequestContextHandler>().SetOrder(OpenIddict.Validation.AspNetCore.OpenIddictValidationAspNetCoreHandlers.ResolveRequestUri.Descriptor.Order - 1);
});
if (hideBackOfficeTokens)
{
options.AddEventHandler<OpenIddictValidationEvents.ProcessAuthenticationContext>(configuration =>
{
configuration
.UseSingletonHandler<HideBackOfficeTokensHandler>()
// IMPORTANT: the handler must be AFTER the built-in query string handler, because the client-side SignalR library sometimes appends access tokens to the query string.
.SetOrder(OpenIddict.Validation.AspNetCore.OpenIddictValidationAspNetCoreHandlers.ExtractAccessTokenFromQueryString.Descriptor.Order + 1);
});
}
});
builder.Services.AddRecurringBackgroundJob<OpenIddictCleanupJob>();
builder.Services.ConfigureOptions<ConfigureOpenIddict>();
if (hideBackOfficeTokens)
{
builder.AddNotificationHandler<UserLogoutSuccessNotification, HideBackOfficeTokensHandler>();
}
}
}
@@ -0,0 +1,148 @@
using Umbraco.Cms.Core.DeliveryApi;
using Umbraco.Cms.Core.Models.PublishedContent;
using Umbraco.Extensions;
namespace Umbraco.Cms.Api.Common.Rendering;
public class ElementOnlyOutputExpansionStrategy : IOutputExpansionStrategy
{
protected const string All = "$all";
protected const string None = "";
protected const string ExpandParameterName = "expand";
protected const string FieldsParameterName = "fields";
private readonly IApiPropertyRenderer _propertyRenderer;
protected Stack<Node?> ExpandProperties { get; } = new();
protected Stack<Node?> IncludeProperties { get; } = new();
public ElementOnlyOutputExpansionStrategy(
IApiPropertyRenderer propertyRenderer)
{
_propertyRenderer = propertyRenderer;
}
public virtual IDictionary<string, object?> MapContentProperties(IPublishedContent content)
=> content.ItemType == PublishedItemType.Content
? MapProperties(content.Properties)
: throw new ArgumentException($"Invalid item type. This method can only be used with item type {nameof(PublishedItemType.Content)}, got: {content.ItemType}");
public virtual IDictionary<string, object?> MapMediaProperties(IPublishedContent media, bool skipUmbracoProperties = true)
{
if (media.ItemType != PublishedItemType.Media)
{
throw new ArgumentException($"Invalid item type. This method can only be used with item type {PublishedItemType.Media}, got: {media.ItemType}");
}
IPublishedProperty[] properties = media
.Properties
.Where(p => skipUmbracoProperties is false || p.Alias.StartsWith("umbraco") is false)
.ToArray();
return properties.Any()
? MapProperties(properties)
: new Dictionary<string, object?>();
}
public virtual IDictionary<string, object?> MapElementProperties(IPublishedElement element)
=> MapProperties(element.Properties, true);
private IDictionary<string, object?> MapProperties(IEnumerable<IPublishedProperty> properties, bool forceExpandProperties = false)
{
Node? currentExpandProperties = ExpandProperties.Count > 0 ? ExpandProperties.Peek() : null;
if (ExpandProperties.Count > 1 && currentExpandProperties is null && forceExpandProperties is false)
{
return new Dictionary<string, object?>();
}
Node? currentIncludeProperties = IncludeProperties.Count > 0 ? IncludeProperties.Peek() : null;
var result = new Dictionary<string, object?>();
foreach (IPublishedProperty property in properties)
{
Node? nextIncludeProperties = GetNextProperties(currentIncludeProperties, property.Alias);
if (currentIncludeProperties is not null && currentIncludeProperties.Items.Any() && nextIncludeProperties is null)
{
continue;
}
Node? nextExpandProperties = GetNextProperties(currentExpandProperties, property.Alias);
IncludeProperties.Push(nextIncludeProperties);
ExpandProperties.Push(nextExpandProperties);
result[property.Alias] = GetPropertyValue(property);
ExpandProperties.Pop();
IncludeProperties.Pop();
}
return result;
}
private Node? GetNextProperties(Node? currentProperties, string propertyAlias)
=> currentProperties?.Items.FirstOrDefault(i => i.Key == All)
?? currentProperties?.Items.FirstOrDefault(i => i.Key == "properties")?.Items.FirstOrDefault(i => i.Key == All || i.Key == propertyAlias);
private object? GetPropertyValue(IPublishedProperty property)
=> _propertyRenderer.GetPropertyValue(property, ExpandProperties.Peek() is not null);
protected sealed class Node
{
public string Key { get; private set; } = string.Empty;
public List<Node> Items { get; } = new();
public static Node Parse(string value)
{
// verify that there are as many start brackets as there are end brackets
if (value.CountOccurrences("[") != value.CountOccurrences("]"))
{
throw new ArgumentException("Value did not contain an equal number of start and end brackets");
}
// verify that the value does not start with a start bracket
if (value.StartsWith("["))
{
throw new ArgumentException("Value cannot start with a bracket");
}
// verify that there are no empty brackets
if (value.Contains("[]"))
{
throw new ArgumentException("Value cannot contain empty brackets");
}
var stack = new Stack<Node>();
var root = new Node { Key = "root" };
stack.Push(root);
var currentNode = new Node();
root.Items.Add(currentNode);
foreach (char c in value)
{
switch (c)
{
case '[': // Start a new node, child of the current node
stack.Push(currentNode);
currentNode = new Node();
stack.Peek().Items.Add(currentNode);
break;
case ',': // Start a new node, but at the same level of the current node
currentNode = new Node();
stack.Peek().Items.Add(currentNode);
break;
case ']': // Back to parent of the current node
currentNode = stack.Pop();
break;
default: // Add char to current node key
currentNode.Key += c;
break;
}
}
return root;
}
}
}
@@ -35,28 +35,35 @@ public static class UmbracoBuilderExtensions
builder.Services.AddScoped<IRequestStartItemProvider, RequestStartItemProvider>();
builder.Services.AddScoped<RequestContextOutputExpansionStrategy>();
builder.Services.AddScoped<RequestContextOutputExpansionStrategyV2>();
builder.Services.AddScoped<IOutputExpansionStrategy>(provider =>
{
HttpContext? httpContext = provider.GetRequiredService<IHttpContextAccessor>().HttpContext;
ApiVersion? apiVersion = httpContext?.GetRequestedApiVersion();
if (apiVersion is null)
{
return provider.GetRequiredService<RequestContextOutputExpansionStrategyV2>();
}
// V1 of the Delivery API uses a different expansion strategy than V2+
return apiVersion.MajorVersion == 1
? provider.GetRequiredService<RequestContextOutputExpansionStrategy>()
: provider.GetRequiredService<RequestContextOutputExpansionStrategyV2>();
});
builder.Services.AddUnique<IOutputExpansionStrategy>(
provider =>
{
HttpContext? httpContext = provider.GetRequiredService<IHttpContextAccessor>().HttpContext;
ApiVersion? apiVersion = httpContext?.GetRequestedApiVersion();
if (apiVersion is null)
{
return provider.GetRequiredService<RequestContextOutputExpansionStrategyV2>();
}
// V1 of the Delivery API uses a different expansion strategy than V2+
return apiVersion.MajorVersion == 1
? provider.GetRequiredService<RequestContextOutputExpansionStrategy>()
: provider.GetRequiredService<RequestContextOutputExpansionStrategyV2>();
},
ServiceLifetime.Scoped);
builder.Services.AddSingleton<IRequestCultureService, RequestCultureService>();
builder.Services.AddSingleton<IRequestSegmmentService, RequestSegmentService>();
builder.Services.AddSingleton<IRequestSegmentService, RequestSegmentService>();
builder.Services.AddSingleton<IRequestRoutingService, RequestRoutingService>();
builder.Services.AddSingleton<IRequestRedirectService, RequestRedirectService>();
builder.Services.AddSingleton<IRequestPreviewService, RequestPreviewService>();
builder.Services.AddSingleton<IOutputExpansionStrategyAccessor, RequestContextOutputExpansionStrategyAccessor>();
// Webooks register a more basic implementation, remove it.
builder.Services.AddUnique<IOutputExpansionStrategyAccessor, RequestContextOutputExpansionStrategyAccessor>(ServiceLifetime.Singleton);
builder.Services.AddSingleton<IRequestStartItemProviderAccessor, RequestContextRequestStartItemProviderAccessor>();
builder.Services.AddSingleton<IApiAccessService, ApiAccessService>();
builder.Services.AddSingleton<IApiContentQueryService, ApiContentQueryService>();
builder.Services.AddSingleton<IApiContentQueryProvider, ApiContentQueryProvider>();
@@ -1,62 +1,25 @@
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using Umbraco.Cms.Api.Common.Rendering;
using Umbraco.Cms.Core.DeliveryApi;
using Umbraco.Cms.Core.Models.PublishedContent;
using Umbraco.Extensions;
namespace Umbraco.Cms.Api.Delivery.Rendering;
internal sealed class RequestContextOutputExpansionStrategyV2 : IOutputExpansionStrategy
internal sealed class RequestContextOutputExpansionStrategyV2 : ElementOnlyOutputExpansionStrategy, IOutputExpansionStrategy
{
private const string All = "$all";
private const string None = "";
private const string ExpandParameterName = "expand";
private const string FieldsParameterName = "fields";
private readonly IApiPropertyRenderer _propertyRenderer;
private readonly ILogger<RequestContextOutputExpansionStrategyV2> _logger;
private readonly Stack<Node?> _expandProperties;
private readonly Stack<Node?> _includeProperties;
public RequestContextOutputExpansionStrategyV2(
IHttpContextAccessor httpContextAccessor,
IApiPropertyRenderer propertyRenderer,
ILogger<RequestContextOutputExpansionStrategyV2> logger)
: base(propertyRenderer)
{
_propertyRenderer = propertyRenderer;
_logger = logger;
_expandProperties = new Stack<Node?>();
_includeProperties = new Stack<Node?>();
InitializeExpandAndInclude(httpContextAccessor);
}
public IDictionary<string, object?> MapContentProperties(IPublishedContent content)
=> content.ItemType == PublishedItemType.Content
? MapProperties(content.Properties)
: throw new ArgumentException($"Invalid item type. This method can only be used with item type {nameof(PublishedItemType.Content)}, got: {content.ItemType}");
public IDictionary<string, object?> MapMediaProperties(IPublishedContent media, bool skipUmbracoProperties = true)
{
if (media.ItemType != PublishedItemType.Media)
{
throw new ArgumentException($"Invalid item type. This method can only be used with item type {PublishedItemType.Media}, got: {media.ItemType}");
}
IPublishedProperty[] properties = media
.Properties
.Where(p => skipUmbracoProperties is false || p.Alias.StartsWith("umbraco") is false)
.ToArray();
return properties.Any()
? MapProperties(properties)
: new Dictionary<string, object?>();
}
public IDictionary<string, object?> MapElementProperties(IPublishedElement element)
=> MapProperties(element.Properties, true);
private void InitializeExpandAndInclude(IHttpContextAccessor httpContextAccessor)
{
string? QueryValue(string key) => httpContextAccessor.HttpContext?.Request.Query[key];
@@ -66,7 +29,7 @@ internal sealed class RequestContextOutputExpansionStrategyV2 : IOutputExpansion
try
{
_expandProperties.Push(Node.Parse(toExpand));
ExpandProperties.Push(Node.Parse(toExpand));
}
catch (ArgumentException ex)
{
@@ -76,7 +39,7 @@ internal sealed class RequestContextOutputExpansionStrategyV2 : IOutputExpansion
try
{
_includeProperties.Push(Node.Parse(toInclude));
IncludeProperties.Push(Node.Parse(toInclude));
}
catch (ArgumentException ex)
{
@@ -84,102 +47,4 @@ internal sealed class RequestContextOutputExpansionStrategyV2 : IOutputExpansion
throw new ArgumentException($"Could not parse the '{FieldsParameterName}' parameter: {ex.Message}");
}
}
private IDictionary<string, object?> MapProperties(IEnumerable<IPublishedProperty> properties, bool forceExpandProperties = false)
{
Node? currentExpandProperties = _expandProperties.Peek();
if (_expandProperties.Count > 1 && currentExpandProperties is null && forceExpandProperties is false)
{
return new Dictionary<string, object?>();
}
Node? currentIncludeProperties = _includeProperties.Peek();
var result = new Dictionary<string, object?>();
foreach (IPublishedProperty property in properties)
{
Node? nextIncludeProperties = GetNextProperties(currentIncludeProperties, property.Alias);
if (currentIncludeProperties is not null && currentIncludeProperties.Items.Any() && nextIncludeProperties is null)
{
continue;
}
Node? nextExpandProperties = GetNextProperties(currentExpandProperties, property.Alias);
_includeProperties.Push(nextIncludeProperties);
_expandProperties.Push(nextExpandProperties);
result[property.Alias] = GetPropertyValue(property);
_expandProperties.Pop();
_includeProperties.Pop();
}
return result;
}
private Node? GetNextProperties(Node? currentProperties, string propertyAlias)
=> currentProperties?.Items.FirstOrDefault(i => i.Key == All)
?? currentProperties?.Items.FirstOrDefault(i => i.Key == "properties")?.Items.FirstOrDefault(i => i.Key == All || i.Key == propertyAlias);
private object? GetPropertyValue(IPublishedProperty property)
=> _propertyRenderer.GetPropertyValue(property, _expandProperties.Peek() is not null);
private sealed class Node
{
public string Key { get; private set; } = string.Empty;
public List<Node> Items { get; } = new();
public static Node Parse(string value)
{
// verify that there are as many start brackets as there are end brackets
if (value.CountOccurrences("[") != value.CountOccurrences("]"))
{
throw new ArgumentException("Value did not contain an equal number of start and end brackets");
}
// verify that the value does not start with a start bracket
if (value.StartsWith("["))
{
throw new ArgumentException("Value cannot start with a bracket");
}
// verify that there are no empty brackets
if (value.Contains("[]"))
{
throw new ArgumentException("Value cannot contain empty brackets");
}
var stack = new Stack<Node>();
var root = new Node { Key = "root" };
stack.Push(root);
var currentNode = new Node();
root.Items.Add(currentNode);
foreach (char c in value)
{
switch (c)
{
case '[': // Start a new node, child of the current node
stack.Push(currentNode);
currentNode = new Node();
stack.Peek().Items.Add(currentNode);
break;
case ',': // Start a new node, but at the same level of the current node
currentNode = new Node();
stack.Peek().Items.Add(currentNode);
break;
case ']': // Back to parent of the current node
currentNode = stack.Pop();
break;
default: // Add char to current node key
currentNode.Key += c;
break;
}
}
return root;
}
}
}
@@ -1,31 +1,60 @@
using Asp.Versioning;
using Asp.Versioning;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.DependencyInjection;
using Umbraco.Cms.Api.Management.ViewModels.Document;
using Umbraco.Cms.Core.Actions;
using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Core.Mapping;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Security.Authorization;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Web.Common.Authorization;
using Umbraco.Extensions;
namespace Umbraco.Cms.Api.Management.Controllers.Document;
[ApiVersion("1.0")]
public class DomainsController : DocumentControllerBase
{
private readonly IAuthorizationService _authorizationService;
private readonly IDomainService _domainService;
private readonly IUmbracoMapper _umbracoMapper;
public DomainsController(IDomainService domainService, IUmbracoMapper umbracoMapper)
[ActivatorUtilitiesConstructor]
public DomainsController(IAuthorizationService authorizationService, IDomainService domainService, IUmbracoMapper umbracoMapper)
{
_authorizationService = authorizationService;
_domainService = domainService;
_umbracoMapper = umbracoMapper;
}
[Obsolete("Please use the constructor taking all parameters. Scheduled for removal in Umbraco 18.")]
public DomainsController(IDomainService domainService, IUmbracoMapper umbracoMapper)
: this(
StaticServiceProvider.Instance.GetRequiredService<IAuthorizationService>(),
domainService,
umbracoMapper)
{
}
[MapToApiVersion("1.0")]
[HttpGet("{id:guid}/domains")]
[ProducesResponseType(typeof(DomainsResponseModel), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
public async Task<IActionResult> Domains(CancellationToken cancellationToken, Guid id)
{
AuthorizationResult authorizationResult = await _authorizationService.AuthorizeResourceAsync(
User,
ContentPermissionResource.WithKeys(ActionBrowse.ActionLetter, id),
AuthorizationPolicies.ContentPermissionByResource);
if (!authorizationResult.Succeeded)
{
return Forbidden();
}
IDomain[] assignedDomains = (await _domainService.GetAssignedDomainsAsync(id, true))
.OrderBy(d => d.SortOrder)
.ToArray();
@@ -1,13 +1,19 @@
using Asp.Versioning;
using Asp.Versioning;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.DependencyInjection;
using Umbraco.Cms.Api.Management.Factories;
using Umbraco.Cms.Api.Management.ViewModels.Document;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Actions;
using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Core.Mapping;
using Umbraco.Cms.Core.Models.ContentEditing;
using Umbraco.Cms.Core.Security.Authorization;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Services.OperationStatus;
using Umbraco.Cms.Web.Common.Authorization;
using Umbraco.Extensions;
namespace Umbraco.Cms.Api.Management.Controllers.Document;
@@ -15,17 +21,30 @@ namespace Umbraco.Cms.Api.Management.Controllers.Document;
[ApiVersion("1.0")]
public class UpdateDomainsController : DocumentControllerBase
{
private readonly IAuthorizationService _authorizationService;
private readonly IDomainService _domainService;
private readonly IUmbracoMapper _umbracoMapper;
private readonly IDomainPresentationFactory _domainPresentationFactory;
public UpdateDomainsController(IDomainService domainService, IUmbracoMapper umbracoMapper, IDomainPresentationFactory domainPresentationFactory)
[ActivatorUtilitiesConstructor]
public UpdateDomainsController(IAuthorizationService authorizationService, IDomainService domainService, IUmbracoMapper umbracoMapper, IDomainPresentationFactory domainPresentationFactory)
{
_authorizationService = authorizationService;
_domainService = domainService;
_umbracoMapper = umbracoMapper;
_domainPresentationFactory = domainPresentationFactory;
}
[Obsolete("Please use the constructor taking all parameters. Scheduled for removal in Umbraco 18.")]
public UpdateDomainsController(IDomainService domainService, IUmbracoMapper umbracoMapper, IDomainPresentationFactory domainPresentationFactory)
: this(
StaticServiceProvider.Instance.GetRequiredService<IAuthorizationService>(),
domainService,
umbracoMapper,
domainPresentationFactory)
{
}
[MapToApiVersion("1.0")]
[HttpPut("{id:guid}/domains")]
[ProducesResponseType(StatusCodes.Status200OK)]
@@ -37,6 +56,16 @@ public class UpdateDomainsController : DocumentControllerBase
Guid id,
UpdateDomainsRequestModel updateModel)
{
AuthorizationResult authorizationResult = await _authorizationService.AuthorizeResourceAsync(
User,
ContentPermissionResource.WithKeys(ActionAssignDomain.ActionLetter, id),
AuthorizationPolicies.ContentPermissionByResource);
if (!authorizationResult.Succeeded)
{
return Forbidden();
}
DomainsUpdateModel domainsUpdateModel = _umbracoMapper.Map<DomainsUpdateModel>(updateModel)!;
Attempt<DomainUpdateResult, DomainOperationStatus> result = await _domainService.UpdateDomainsAsync(id, domainsUpdateModel);
@@ -1,33 +1,63 @@
using Asp.Versioning;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.DependencyInjection;
using Umbraco.Cms.Api.Management.ViewModels.Document;
using Umbraco.Cms.Core.Actions;
using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Security;
using Umbraco.Cms.Core.Security.Authorization;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Web.Common.Authorization;
using Umbraco.Extensions;
namespace Umbraco.Cms.Api.Management.Controllers.Document;
[ApiVersion("1.0")]
public class UpdateNotificationsController : DocumentControllerBase
{
private readonly IAuthorizationService _authorizationService;
private readonly IContentEditingService _contentEditingService;
private readonly INotificationService _notificationService;
private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor;
public UpdateNotificationsController(IContentEditingService contentEditingService, INotificationService notificationService, IBackOfficeSecurityAccessor backOfficeSecurityAccessor)
[ActivatorUtilitiesConstructor]
public UpdateNotificationsController(IAuthorizationService authorizationService, IContentEditingService contentEditingService, INotificationService notificationService, IBackOfficeSecurityAccessor backOfficeSecurityAccessor)
{
_authorizationService = authorizationService;
_contentEditingService = contentEditingService;
_notificationService = notificationService;
_backOfficeSecurityAccessor = backOfficeSecurityAccessor;
}
[Obsolete("Please use the constructor taking all parameters. Scheduled for removal in Umbraco 18.")]
public UpdateNotificationsController(IContentEditingService contentEditingService, INotificationService notificationService, IBackOfficeSecurityAccessor backOfficeSecurityAccessor)
: this(
StaticServiceProvider.Instance.GetRequiredService<IAuthorizationService>(),
contentEditingService,
notificationService,
backOfficeSecurityAccessor)
{
}
[MapToApiVersion("1.0")]
[HttpPut("{id:guid}/notifications")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
public async Task<IActionResult> UpdateNotifications(CancellationToken cancellationToken, Guid id, UpdateDocumentNotificationsRequestModel updateModel)
{
AuthorizationResult authorizationResult = await _authorizationService.AuthorizeResourceAsync(
User,
ContentPermissionResource.WithKeys(ActionBrowse.ActionLetter, id),
AuthorizationPolicies.ContentPermissionByResource);
if (!authorizationResult.Succeeded)
{
return Forbidden();
}
IContent? content = await _contentEditingService.GetAsync(id);
if (content == null)
{
@@ -1,34 +1,32 @@
using Asp.Versioning;
using Asp.Versioning;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.DependencyInjection;
using Umbraco.Cms.Api.Management.Services.FileSystem;
using Umbraco.Cms.Api.Management.ViewModels.Tree;
using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Core.IO;
using Umbraco.Cms.Core.Services;
namespace Umbraco.Cms.Api.Management.Controllers.PartialView.Tree;
[ApiVersion("1.0")]
public class AncestorsPartialViewTreeController : PartialViewTreeControllerBase
{
private readonly IPartialViewTreeService _partialViewTreeService;
// TODO Remove the static service provider, and replace with base when the other constructors are obsoleted.
public AncestorsPartialViewTreeController(IPartialViewTreeService partialViewTreeService)
: this(partialViewTreeService, StaticServiceProvider.Instance.GetRequiredService<FileSystems>())
=> _partialViewTreeService = partialViewTreeService;
[ActivatorUtilitiesConstructor]
[Obsolete("Please use the other constructor. Scheduled to be removed in Umbraco 19")]
public AncestorsPartialViewTreeController(IPartialViewTreeService partialViewTreeService, FileSystems fileSystems)
: base(partialViewTreeService, fileSystems) =>
_partialViewTreeService = partialViewTreeService;
public AncestorsPartialViewTreeController(IPartialViewTreeService partialViewTreeService)
: base(partialViewTreeService)
{
}
[Obsolete("Please use the other constructor. Scheduled to be removed in Umbraco 19")]
[Obsolete("Please use the constructor taking all parameters. Scheduled to be removed in Umbraco 19.")]
public AncestorsPartialViewTreeController(IPartialViewTreeService partialViewTreeService, FileSystems fileSystems)
: base(partialViewTreeService, fileSystems)
{
}
[Obsolete("Please use the constructor taking all parameters. Scheduled to be removed in Umbraco 19.")]
public AncestorsPartialViewTreeController(FileSystems fileSystems)
: this(StaticServiceProvider.Instance.GetRequiredService<IPartialViewTreeService>(), fileSystems)
: base(fileSystems)
{
}
@@ -1,34 +1,33 @@
using Asp.Versioning;
using Asp.Versioning;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.DependencyInjection;
using Umbraco.Cms.Core.IO;
using Umbraco.Cms.Api.Common.ViewModels.Pagination;
using Umbraco.Cms.Api.Management.Services.FileSystem;
using Umbraco.Cms.Api.Management.ViewModels.Tree;
using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Core.IO;
namespace Umbraco.Cms.Api.Management.Controllers.PartialView.Tree;
[ApiVersion("1.0")]
public class ChildrenPartialViewTreeController : PartialViewTreeControllerBase
{
private readonly IPartialViewTreeService _partialViewTreeService;
// TODO Remove the static service provider, and replace with base when the other constructors are obsoleted.
public ChildrenPartialViewTreeController(IPartialViewTreeService partialViewTreeService)
: this(partialViewTreeService, StaticServiceProvider.Instance.GetRequiredService<FileSystems>())
=> _partialViewTreeService = partialViewTreeService;
[ActivatorUtilitiesConstructor]
[Obsolete("Please use the other constructor. Scheduled to be removed in Umbraco 19")]
public ChildrenPartialViewTreeController(IPartialViewTreeService partialViewTreeService, FileSystems fileSystems)
: base(partialViewTreeService, fileSystems) =>
_partialViewTreeService = partialViewTreeService;
public ChildrenPartialViewTreeController(IPartialViewTreeService partialViewTreeService)
: base(partialViewTreeService)
{
}
[Obsolete("Please use the other constructor. Scheduled to be removed in Umbraco 19")]
[Obsolete("Please use the constructor taking all parameters. Scheduled to be removed in Umbraco 19.")]
public ChildrenPartialViewTreeController(IPartialViewTreeService partialViewTreeService, FileSystems fileSystems)
: base(partialViewTreeService, fileSystems)
{
}
[Obsolete("Please use the constructor taking all parameters. Scheduled to be removed in Umbraco 19.")]
public ChildrenPartialViewTreeController(FileSystems fileSystems)
: this(StaticServiceProvider.Instance.GetRequiredService<IPartialViewTreeService>(), fileSystems)
: base(fileSystems)
{
}
@@ -1,11 +1,9 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.DependencyInjection;
using Umbraco.Cms.Api.Management.Controllers.Tree;
using Umbraco.Cms.Api.Management.Routing;
using Umbraco.Cms.Api.Management.Services.FileSystem;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Core.IO;
using Umbraco.Cms.Web.Common.Authorization;
@@ -16,30 +14,28 @@ namespace Umbraco.Cms.Api.Management.Controllers.PartialView.Tree;
[Authorize(Policy = AuthorizationPolicies.TreeAccessPartialViews)]
public class PartialViewTreeControllerBase : FileSystemTreeControllerBase
{
private readonly IPartialViewTreeService _partialViewTreeService;
// TODO Remove the static service provider, and replace with base when the other constructors are obsoleted.
public PartialViewTreeControllerBase(IPartialViewTreeService partialViewTreeService)
: this(partialViewTreeService, StaticServiceProvider.Instance.GetRequiredService<FileSystems>()) =>
_partialViewTreeService = partialViewTreeService;
: base(partialViewTreeService)
{
FileSystem = null!;
}
// FileSystem is required therefore, we can't remove it without some wizadry. When obsoletion is due, remove this.
[ActivatorUtilitiesConstructor]
[Obsolete("Scheduled for removal in Umbraco 18.")]
// FileSystem is required therefore, we can't remove it without some wizardry. When obsoletion is due, remove this.
[Obsolete("Please use the constructor taking all parameters. Scheduled to be removed in Umbraco 19.")]
public PartialViewTreeControllerBase(IPartialViewTreeService partialViewTreeService, FileSystems fileSystems)
: base(partialViewTreeService)
{
_partialViewTreeService = partialViewTreeService;
FileSystem = fileSystems.PartialViewsFileSystem ??
throw new ArgumentException("Missing scripts file system", nameof(fileSystems));
}
[Obsolete("Please use the other constructor. Scheduled to be removed in Umbraco 18.")]
[Obsolete("Please use the constructor taking all parameters. Scheduled to be removed in Umbraco 19.")]
public PartialViewTreeControllerBase(FileSystems fileSystems)
: this(StaticServiceProvider.Instance.GetRequiredService<IPartialViewTreeService>())
: base()
=> FileSystem = fileSystems.PartialViewsFileSystem ??
throw new ArgumentException("Missing scripts file system", nameof(fileSystems));
[Obsolete("Included in the service class. Scheduled to be removed in Umbraco 18.")]
[Obsolete("Included in the service class. Scheduled to be removed in Umbraco 19.")]
protected override IFileSystem FileSystem { get; }
}
@@ -1,34 +1,33 @@
using Asp.Versioning;
using Asp.Versioning;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.DependencyInjection;
using Umbraco.Cms.Core.IO;
using Umbraco.Cms.Api.Common.ViewModels.Pagination;
using Umbraco.Cms.Api.Management.Services.FileSystem;
using Umbraco.Cms.Api.Management.ViewModels.Tree;
using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Core.IO;
namespace Umbraco.Cms.Api.Management.Controllers.PartialView.Tree;
[ApiVersion("1.0")]
public class RootPartialViewTreeController : PartialViewTreeControllerBase
{
private readonly IPartialViewTreeService _partialViewTreeService;
// TODO Remove the static service provider, and replace with base when the other constructors are obsoleted.
public RootPartialViewTreeController(IPartialViewTreeService partialViewTreeService)
: this(partialViewTreeService, StaticServiceProvider.Instance.GetRequiredService<FileSystems>())
=> _partialViewTreeService = partialViewTreeService;
[ActivatorUtilitiesConstructor]
[Obsolete("Please use the other constructor. Scheduled to be removed in Umbraco 19")]
public RootPartialViewTreeController(IPartialViewTreeService partialViewTreeService, FileSystems fileSystems)
: base(partialViewTreeService, fileSystems) =>
_partialViewTreeService = partialViewTreeService;
public RootPartialViewTreeController(IPartialViewTreeService partialViewTreeService)
: base(partialViewTreeService)
{
}
[Obsolete("Please use the other constructor. Scheduled to be removed in Umbraco 19")]
[Obsolete("Please use the constructor taking all parameters. Scheduled to be removed in Umbraco 19.")]
public RootPartialViewTreeController(IPartialViewTreeService partialViewTreeService, FileSystems fileSystems)
: base(partialViewTreeService, fileSystems)
{
}
[Obsolete("Please use the constructor taking all parameters. Scheduled to be removed in Umbraco 19.")]
public RootPartialViewTreeController(FileSystems fileSystems)
: this(StaticServiceProvider.Instance.GetRequiredService<IPartialViewTreeService>(), fileSystems)
: base(fileSystems)
{
}
@@ -1,32 +1,31 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.DependencyInjection;
using Umbraco.Cms.Api.Common.ViewModels.Pagination;
using Umbraco.Cms.Api.Management.Services.FileSystem;
using Umbraco.Cms.Api.Management.ViewModels.Tree;
using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Core.IO;
namespace Umbraco.Cms.Api.Management.Controllers.PartialView.Tree;
public class SiblingsPartialViewTreeController : PartialViewTreeControllerBase
{
private readonly IPartialViewTreeService _partialViewTreeService;
// TODO Remove the static service provider, and replace with base when the other constructors are obsoleted.
public SiblingsPartialViewTreeController(IPartialViewTreeService partialViewTreeService)
: this(partialViewTreeService, StaticServiceProvider.Instance.GetRequiredService<FileSystems>())
=> _partialViewTreeService = partialViewTreeService;
[ActivatorUtilitiesConstructor]
[Obsolete("Please use the other constructor. Scheduled to be removed in Umbraco 19")]
public SiblingsPartialViewTreeController(IPartialViewTreeService partialViewTreeService, FileSystems fileSystems)
: base(partialViewTreeService, fileSystems) =>
_partialViewTreeService = partialViewTreeService;
public SiblingsPartialViewTreeController(IPartialViewTreeService partialViewTreeService)
: base(partialViewTreeService)
{
}
[Obsolete("Please use the other constructor. Scheduled to be removed in Umbraco 19")]
[Obsolete("Please use the constructor taking all parameters. Scheduled to be removed in Umbraco 19.")]
public SiblingsPartialViewTreeController(IPartialViewTreeService partialViewTreeService, FileSystems fileSystems)
: base(partialViewTreeService, fileSystems)
{
}
[Obsolete("Please use the constructor taking all parameters. Scheduled to be removed in Umbraco 19.")]
public SiblingsPartialViewTreeController(FileSystems fileSystems)
: this(StaticServiceProvider.Instance.GetRequiredService<IPartialViewTreeService>(), fileSystems)
: base(fileSystems)
{
}
@@ -1,10 +1,9 @@
using Asp.Versioning;
using Asp.Versioning;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.DependencyInjection;
using Umbraco.Cms.Api.Management.Services.FileSystem;
using Umbraco.Cms.Api.Management.ViewModels.Tree;
using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Core.IO;
namespace Umbraco.Cms.Api.Management.Controllers.Script.Tree;
@@ -12,22 +11,22 @@ namespace Umbraco.Cms.Api.Management.Controllers.Script.Tree;
[ApiVersion("1.0")]
public class AncestorsScriptTreeController : ScriptTreeControllerBase
{
private readonly IScriptTreeService _scriptTreeService;
// TODO Remove the static service provider, and replace with base when the other constructors are obsoleted.
public AncestorsScriptTreeController(IScriptTreeService scriptTreeService)
: this(scriptTreeService, StaticServiceProvider.Instance.GetRequiredService<FileSystems>())
=> _scriptTreeService = scriptTreeService;
[ActivatorUtilitiesConstructor]
[Obsolete("Please use the other constructor. Scheduled to be removed in Umbraco 19")]
public AncestorsScriptTreeController(IScriptTreeService scriptTreeService, FileSystems fileSystems)
: base(scriptTreeService, fileSystems) =>
_scriptTreeService = scriptTreeService;
public AncestorsScriptTreeController(IScriptTreeService scriptTreeService)
: base(scriptTreeService)
{
}
[Obsolete("Please use the other constructor. Scheduled to be removed in Umbraco 19")]
[Obsolete("Please use the constructor taking all parameters. Scheduled to be removed in Umbraco 19.")]
public AncestorsScriptTreeController(IScriptTreeService scriptTreeService, FileSystems fileSystems)
: base(scriptTreeService, fileSystems)
{
}
[Obsolete("Please use the constructor taking all parameters. Scheduled to be removed in Umbraco 19.")]
public AncestorsScriptTreeController(FileSystems fileSystems)
: this(StaticServiceProvider.Instance.GetRequiredService<IScriptTreeService>(), fileSystems)
: base(fileSystems)
{
}
@@ -1,34 +1,33 @@
using Asp.Versioning;
using Asp.Versioning;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.DependencyInjection;
using Umbraco.Cms.Core.IO;
using Umbraco.Cms.Api.Common.ViewModels.Pagination;
using Umbraco.Cms.Api.Management.Services.FileSystem;
using Umbraco.Cms.Api.Management.ViewModels.Tree;
using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Core.IO;
namespace Umbraco.Cms.Api.Management.Controllers.Script.Tree;
[ApiVersion("1.0")]
public class ChildrenScriptTreeController : ScriptTreeControllerBase
{
private readonly IScriptTreeService _scriptTreeService;
// TODO Remove the static service provider, and replace with base when the other constructors are obsoleted.
public ChildrenScriptTreeController(IScriptTreeService scriptTreeService)
: this(scriptTreeService, StaticServiceProvider.Instance.GetRequiredService<FileSystems>())
=> _scriptTreeService = scriptTreeService;
[ActivatorUtilitiesConstructor]
[Obsolete("Please use the other constructor. Scheduled to be removed in Umbraco 19")]
public ChildrenScriptTreeController(IScriptTreeService scriptTreeService, FileSystems fileSystems)
: base(scriptTreeService, fileSystems) =>
_scriptTreeService = scriptTreeService;
public ChildrenScriptTreeController(IScriptTreeService scriptTreeService)
: base(scriptTreeService)
{
}
[Obsolete("Please use the other constructor. Scheduled to be removed in Umbraco 19")]
[Obsolete("Please use the constructor taking all parameters. Scheduled to be removed in Umbraco 19.")]
public ChildrenScriptTreeController(IScriptTreeService scriptTreeService, FileSystems fileSystems)
: base(scriptTreeService, fileSystems)
{
}
[Obsolete("Please use the constructor taking all parameters. Scheduled to be removed in Umbraco 19.")]
public ChildrenScriptTreeController(FileSystems fileSystems)
: this(StaticServiceProvider.Instance.GetRequiredService<IScriptTreeService>(), fileSystems)
: base(fileSystems)
{
}
@@ -1,34 +1,33 @@
using Asp.Versioning;
using Asp.Versioning;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.DependencyInjection;
using Umbraco.Cms.Core.IO;
using Umbraco.Cms.Api.Common.ViewModels.Pagination;
using Umbraco.Cms.Api.Management.Services.FileSystem;
using Umbraco.Cms.Api.Management.ViewModels.Tree;
using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Core.IO;
namespace Umbraco.Cms.Api.Management.Controllers.Script.Tree;
[ApiVersion("1.0")]
public class RootScriptTreeController : ScriptTreeControllerBase
{
private readonly IScriptTreeService _scriptTreeService;
// TODO Remove the static service provider, and replace with base when the other constructors are obsoleted.
public RootScriptTreeController(IScriptTreeService scriptTreeService)
: this(scriptTreeService, StaticServiceProvider.Instance.GetRequiredService<FileSystems>())
=> _scriptTreeService = scriptTreeService;
[ActivatorUtilitiesConstructor]
[Obsolete("Please use the other constructor. Scheduled to be removed in Umbraco 19")]
public RootScriptTreeController(IScriptTreeService scriptTreeService, FileSystems fileSystems)
: base(scriptTreeService, fileSystems) =>
_scriptTreeService = scriptTreeService;
public RootScriptTreeController(IScriptTreeService scriptTreeService)
: base(scriptTreeService)
{
}
[Obsolete("Please use the other constructor. Scheduled to be removed in Umbraco 19")]
[Obsolete("Please use the constructor taking all parameters. Scheduled to be removed in Umbraco 19.")]
public RootScriptTreeController(IScriptTreeService scriptTreeService, FileSystems fileSystems)
: base(scriptTreeService, fileSystems)
{
}
[Obsolete("Please use the constructor taking all parameters. Scheduled to be removed in Umbraco 19.")]
public RootScriptTreeController(FileSystems fileSystems)
: this(StaticServiceProvider.Instance.GetRequiredService<IScriptTreeService>(), fileSystems)
: base(fileSystems)
{
}
@@ -1,11 +1,9 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.DependencyInjection;
using Umbraco.Cms.Api.Management.Controllers.Tree;
using Umbraco.Cms.Api.Management.Routing;
using Umbraco.Cms.Api.Management.Services.FileSystem;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Core.IO;
using Umbraco.Cms.Web.Common.Authorization;
@@ -16,30 +14,28 @@ namespace Umbraco.Cms.Api.Management.Controllers.Script.Tree;
[Authorize(Policy = AuthorizationPolicies.TreeAccessScripts)]
public class ScriptTreeControllerBase : FileSystemTreeControllerBase
{
private readonly IScriptTreeService _scriptTreeService;
// TODO Remove the static service provider, and replace with base when the other constructors are obsoleted.
public ScriptTreeControllerBase(IScriptTreeService scriptTreeService)
: this(scriptTreeService, StaticServiceProvider.Instance.GetRequiredService<FileSystems>()) =>
_scriptTreeService = scriptTreeService;
: base(scriptTreeService)
{
FileSystem = null!;
}
// FileSystem is required therefore, we can't remove it without some wizadry. When obsoletion is due, remove this.
[ActivatorUtilitiesConstructor]
[Obsolete("Scheduled for removal in Umbraco 18.")]
// FileSystem is required therefore, we can't remove it without some wizardry. When obsoletion is due, remove this.
[Obsolete("Please use the constructor taking all parameters. Scheduled to be removed in Umbraco 19.")]
public ScriptTreeControllerBase(IScriptTreeService scriptTreeService, FileSystems fileSystems)
: base(scriptTreeService)
{
_scriptTreeService = scriptTreeService;
FileSystem = fileSystems.ScriptsFileSystem ??
throw new ArgumentException("Missing scripts file system", nameof(fileSystems));
}
[Obsolete("Please use the other constructor. Scheduled to be removed in Umbraco 18.")]
[Obsolete("Please use the constructor taking all parameters. Scheduled to be removed in Umbraco 19.")]
public ScriptTreeControllerBase(FileSystems fileSystems)
: this(StaticServiceProvider.Instance.GetRequiredService<IScriptTreeService>())
: base()
=> FileSystem = fileSystems.ScriptsFileSystem ??
throw new ArgumentException("Missing scripts file system", nameof(fileSystems));
[Obsolete("Included in the service class. Scheduled to be removed in Umbraco 18.")]
[Obsolete("Included in the service class. Scheduled to be removed in Umbraco 19.")]
protected override IFileSystem FileSystem { get; }
}
@@ -1,32 +1,31 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.DependencyInjection;
using Umbraco.Cms.Api.Common.ViewModels.Pagination;
using Umbraco.Cms.Api.Management.Services.FileSystem;
using Umbraco.Cms.Api.Management.ViewModels.Tree;
using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Core.IO;
namespace Umbraco.Cms.Api.Management.Controllers.Script.Tree;
public class SiblingsScriptTreeController : ScriptTreeControllerBase
{
private readonly IScriptTreeService _scriptTreeService;
// TODO Remove the static service provider, and replace with base when the other constructors are obsoleted.
public SiblingsScriptTreeController(IScriptTreeService scriptTreeService)
: this(scriptTreeService, StaticServiceProvider.Instance.GetRequiredService<FileSystems>())
=> _scriptTreeService = scriptTreeService;
[ActivatorUtilitiesConstructor]
[Obsolete("Please use the other constructor. Scheduled to be removed in Umbraco 19")]
public SiblingsScriptTreeController(IScriptTreeService scriptTreeService, FileSystems fileSystems)
: base(scriptTreeService, fileSystems) =>
_scriptTreeService = scriptTreeService;
public SiblingsScriptTreeController(IScriptTreeService scriptTreeService)
: base(scriptTreeService)
{
}
[Obsolete("Please use the other constructor. Scheduled to be removed in Umbraco 19")]
[Obsolete("Please use the constructor taking all parameters. Scheduled to be removed in Umbraco 19.")]
public SiblingsScriptTreeController(IScriptTreeService scriptTreeService, FileSystems fileSystems)
: base(scriptTreeService, fileSystems)
{
}
[Obsolete("Please use the constructor taking all parameters. Scheduled to be removed in Umbraco 19.")]
public SiblingsScriptTreeController(FileSystems fileSystems)
: this(StaticServiceProvider.Instance.GetRequiredService<IScriptTreeService>(), fileSystems)
: base(fileSystems)
{
}
@@ -1,10 +1,9 @@
using Asp.Versioning;
using Asp.Versioning;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.DependencyInjection;
using Umbraco.Cms.Api.Management.Services.FileSystem;
using Umbraco.Cms.Api.Management.ViewModels.Tree;
using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Core.IO;
namespace Umbraco.Cms.Api.Management.Controllers.Stylesheet.Tree;
@@ -12,22 +11,22 @@ namespace Umbraco.Cms.Api.Management.Controllers.Stylesheet.Tree;
[ApiVersion("1.0")]
public class AncestorsStylesheetTreeController : StylesheetTreeControllerBase
{
private readonly IStyleSheetTreeService _styleSheetTreeService;
// TODO Remove the static service provider, and replace with base when the other constructors are obsoleted.
public AncestorsStylesheetTreeController(IStyleSheetTreeService styleSheetTreeService)
: this(styleSheetTreeService, StaticServiceProvider.Instance.GetRequiredService<FileSystems>())
=> _styleSheetTreeService = styleSheetTreeService;
[ActivatorUtilitiesConstructor]
[Obsolete("Please use the other constructor. Scheduled to be removed in Umbraco 19")]
public AncestorsStylesheetTreeController(IStyleSheetTreeService styleSheetTreeService, FileSystems fileSystems)
: base(styleSheetTreeService, fileSystems) =>
_styleSheetTreeService = styleSheetTreeService;
public AncestorsStylesheetTreeController(IStyleSheetTreeService styleSheetTreeService)
: base(styleSheetTreeService)
{
}
[Obsolete("Please use the other constructor. Scheduled to be removed in Umbraco 19")]
[Obsolete("Please use the constructor taking all parameters. Scheduled to be removed in Umbraco 19.")]
public AncestorsStylesheetTreeController(IStyleSheetTreeService styleSheetTreeService, FileSystems fileSystems)
: base(styleSheetTreeService, fileSystems)
{
}
[Obsolete("Please use the constructor taking all parameters. Scheduled to be removed in Umbraco 19.")]
public AncestorsStylesheetTreeController(FileSystems fileSystems)
: this(StaticServiceProvider.Instance.GetRequiredService<IStyleSheetTreeService>(), fileSystems)
: base(fileSystems)
{
}
@@ -1,36 +1,36 @@
using Asp.Versioning;
using Asp.Versioning;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.DependencyInjection;
using Umbraco.Cms.Core.IO;
using Umbraco.Cms.Api.Common.ViewModels.Pagination;
using Umbraco.Cms.Api.Management.Services.FileSystem;
using Umbraco.Cms.Api.Management.ViewModels.Tree;
using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Core.IO;
namespace Umbraco.Cms.Api.Management.Controllers.Stylesheet.Tree;
[ApiVersion("1.0")]
public class ChildrenStylesheetTreeController : StylesheetTreeControllerBase
{
private readonly IStyleSheetTreeService _styleSheetTreeService;
// TODO Remove the static service provider, and replace with base when the other constructors are obsoleted.
public ChildrenStylesheetTreeController(IStyleSheetTreeService styleSheetTreeService)
: this(styleSheetTreeService, StaticServiceProvider.Instance.GetRequiredService<FileSystems>())
=> _styleSheetTreeService = styleSheetTreeService;
[ActivatorUtilitiesConstructor]
[Obsolete("Please use the other constructor. Scheduled to be removed in Umbraco 19")]
public ChildrenStylesheetTreeController(IStyleSheetTreeService styleSheetTreeService, FileSystems fileSystems)
: base(styleSheetTreeService, fileSystems) =>
_styleSheetTreeService = styleSheetTreeService;
[Obsolete("Please use the other constructor. Scheduled to be removed in Umbraco 19")]
public ChildrenStylesheetTreeController(FileSystems fileSystems)
: this(StaticServiceProvider.Instance.GetRequiredService<IStyleSheetTreeService>(), fileSystems)
public ChildrenStylesheetTreeController(IStyleSheetTreeService styleSheetTreeService)
: base(styleSheetTreeService)
{
}
[Obsolete("Please use the constructor taking all parameters. Scheduled to be removed in Umbraco 19.")]
public ChildrenStylesheetTreeController(IStyleSheetTreeService styleSheetTreeService, FileSystems fileSystems)
: base(styleSheetTreeService, fileSystems)
{
}
[Obsolete("Please use the constructor taking all parameters. Scheduled to be removed in Umbraco 19.")]
public ChildrenStylesheetTreeController(FileSystems fileSystems)
: base(fileSystems)
{
}
[HttpGet("children")]
[MapToApiVersion("1.0")]
[ProducesResponseType(typeof(PagedViewModel<FileSystemTreeItemPresentationModel>), StatusCodes.Status200OK)]
@@ -1,34 +1,33 @@
using Asp.Versioning;
using Asp.Versioning;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.DependencyInjection;
using Umbraco.Cms.Core.IO;
using Umbraco.Cms.Api.Common.ViewModels.Pagination;
using Umbraco.Cms.Api.Management.Services.FileSystem;
using Umbraco.Cms.Api.Management.ViewModels.Tree;
using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Core.IO;
namespace Umbraco.Cms.Api.Management.Controllers.Stylesheet.Tree;
[ApiVersion("1.0")]
public class RootStylesheetTreeController : StylesheetTreeControllerBase
{
private readonly IStyleSheetTreeService _styleSheetTreeService;
// TODO Remove the static service provider, and replace with base when the other constructors are obsoleted.
public RootStylesheetTreeController(IStyleSheetTreeService styleSheetTreeService)
: this(styleSheetTreeService, StaticServiceProvider.Instance.GetRequiredService<FileSystems>())
=> _styleSheetTreeService = styleSheetTreeService;
[ActivatorUtilitiesConstructor]
[Obsolete("Please use the other constructor. Scheduled to be removed in Umbraco 19")]
public RootStylesheetTreeController(IStyleSheetTreeService styleSheetTreeService, FileSystems fileSystems)
: base(styleSheetTreeService, fileSystems) =>
_styleSheetTreeService = styleSheetTreeService;
public RootStylesheetTreeController(IStyleSheetTreeService styleSheetTreeService)
: base(styleSheetTreeService)
{
}
[Obsolete("Please use the other constructor. Scheduled to be removed in Umbraco 19")]
[Obsolete("Please use the constructor taking all parameters. Scheduled to be removed in Umbraco 19.")]
public RootStylesheetTreeController(IStyleSheetTreeService styleSheetTreeService, FileSystems fileSystems)
: base(styleSheetTreeService, fileSystems)
{
}
[Obsolete("Please use the constructor taking all parameters. Scheduled to be removed in Umbraco 19.")]
public RootStylesheetTreeController(FileSystems fileSystems)
: this(StaticServiceProvider.Instance.GetRequiredService<IStyleSheetTreeService>(), fileSystems)
: base(fileSystems)
{
}
@@ -1,32 +1,31 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.DependencyInjection;
using Umbraco.Cms.Api.Common.ViewModels.Pagination;
using Umbraco.Cms.Api.Management.Services.FileSystem;
using Umbraco.Cms.Api.Management.ViewModels.Tree;
using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Core.IO;
namespace Umbraco.Cms.Api.Management.Controllers.Stylesheet.Tree;
public class SiblingsStylesheetTreeController : StylesheetTreeControllerBase
{
private readonly IStyleSheetTreeService _styleSheetTreeService;
// TODO Remove the static service provider, and replace with base when the other constructors are obsoleted.
public SiblingsStylesheetTreeController(IStyleSheetTreeService styleSheetTreeService)
: this(styleSheetTreeService, StaticServiceProvider.Instance.GetRequiredService<FileSystems>())
=> _styleSheetTreeService = styleSheetTreeService;
[ActivatorUtilitiesConstructor]
[Obsolete("Please use the other constructor. Scheduled to be removed in Umbraco 19")]
public SiblingsStylesheetTreeController(IStyleSheetTreeService styleSheetTreeService, FileSystems fileSystems)
: base(styleSheetTreeService, fileSystems) =>
_styleSheetTreeService = styleSheetTreeService;
public SiblingsStylesheetTreeController(IStyleSheetTreeService styleSheetTreeService)
: base(styleSheetTreeService)
{
}
[Obsolete("Please use the other constructor. Scheduled to be removed in Umbraco 19")]
[Obsolete("Please use the constructor taking all parameters. Scheduled to be removed in Umbraco 19.")]
public SiblingsStylesheetTreeController(IStyleSheetTreeService styleSheetTreeService, FileSystems fileSystems)
: base(styleSheetTreeService, fileSystems)
{
}
[Obsolete("Please use the constructor taking all parameters. Scheduled to be removed in Umbraco 19.")]
public SiblingsStylesheetTreeController(FileSystems fileSystems)
: this(StaticServiceProvider.Instance.GetRequiredService<IStyleSheetTreeService>(), fileSystems)
: base(fileSystems)
{
}
@@ -1,11 +1,9 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.DependencyInjection;
using Umbraco.Cms.Api.Management.Controllers.Tree;
using Umbraco.Cms.Api.Management.Routing;
using Umbraco.Cms.Api.Management.Services.FileSystem;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Core.IO;
using Umbraco.Cms.Web.Common.Authorization;
@@ -16,30 +14,28 @@ namespace Umbraco.Cms.Api.Management.Controllers.Stylesheet.Tree;
[Authorize(Policy = AuthorizationPolicies.TreeAccessStylesheets)]
public class StylesheetTreeControllerBase : FileSystemTreeControllerBase
{
private readonly IStyleSheetTreeService _styleSheetTreeService;
// TODO Remove the static service provider, and replace with base when the other constructors are obsoleted.
public StylesheetTreeControllerBase(IStyleSheetTreeService styleSheetTreeService)
: this(styleSheetTreeService, StaticServiceProvider.Instance.GetRequiredService<FileSystems>()) =>
_styleSheetTreeService = styleSheetTreeService;
: base(styleSheetTreeService)
{
FileSystem = null!;
}
// FileSystem is required therefore, we can't remove it without some wizadry. When obsoletion is due, remove this.
[ActivatorUtilitiesConstructor]
[Obsolete("Scheduled for removal in Umbraco 18.")]
[Obsolete("Please use the constructor taking all parameters. Scheduled to be removed in Umbraco 19.")]
public StylesheetTreeControllerBase(IStyleSheetTreeService styleSheetTreeService, FileSystems fileSystems)
: base(styleSheetTreeService)
{
_styleSheetTreeService = styleSheetTreeService;
FileSystem = fileSystems.ScriptsFileSystem ??
throw new ArgumentException("Missing scripts file system", nameof(fileSystems));
}
[Obsolete("Please use the other constructor. Scheduled to be removed in Umbraco 18.")]
[Obsolete("Please use the constructor taking all parameters. Scheduled to be removed in Umbraco 19.")]
public StylesheetTreeControllerBase(FileSystems fileSystems)
: this(StaticServiceProvider.Instance.GetRequiredService<IStyleSheetTreeService>())
: base()
=> FileSystem = fileSystems.ScriptsFileSystem ??
throw new ArgumentException("Missing scripts file system", nameof(fileSystems));
[Obsolete("Included in the service class. Scheduled to be removed in Umbraco 18.")]
[Obsolete("Included in the service class. Scheduled to be removed in Umbraco 19.")]
protected override IFileSystem FileSystem { get; }
}
@@ -5,7 +5,6 @@ using Umbraco.Cms.Api.Management.Extensions;
using Umbraco.Cms.Api.Management.Services.FileSystem;
using Umbraco.Cms.Api.Management.ViewModels.FileSystem;
using Umbraco.Cms.Api.Management.ViewModels.Tree;
using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Core.IO;
using Umbraco.Extensions;
@@ -13,23 +12,30 @@ namespace Umbraco.Cms.Api.Management.Controllers.Tree;
public abstract class FileSystemTreeControllerBase : ManagementApiControllerBase
{
private readonly IFileSystemTreeService _fileSystemTreeService;
private readonly IFileSystemTreeService _fileSystemTreeService = null!;
[Obsolete("Has been moved to the individual services. Scheduled to be removed in Umbraco 18.")]
/// <summary>
/// Indicates whether to use the IFileSystemTreeService or the legacy implementation.
/// </summary>
/// <remarks>
/// This is retained to ensure that any controllers outside of the CMS that use this base class with the obsolete constructor
/// continue to function until they can be updated to use the new service.
/// To be removed along with the constructor taking no parameters in Umbraco 19.
/// </remarks>
private readonly bool _useFileSystemTreeService = true;
[Obsolete("Has been moved to the individual services. Scheduled to be removed in Umbraco 19.")]
protected abstract IFileSystem FileSystem { get; }
[ActivatorUtilitiesConstructor]
protected FileSystemTreeControllerBase(IFileSystemTreeService fileSystemTreeService) => _fileSystemTreeService = fileSystemTreeService;
[Obsolete("Use the other constructor. Scheduled for removal in Umbraco 18.")]
protected FileSystemTreeControllerBase()
: this(StaticServiceProvider.Instance.GetRequiredService<IScriptTreeService>())
{
}
[Obsolete("Please use the constructor taking all parameters. Scheduled for removal in Umbraco 19.")]
protected FileSystemTreeControllerBase() => _useFileSystemTreeService = false;
protected Task<ActionResult<PagedViewModel<FileSystemTreeItemPresentationModel>>> GetRoot(int skip, int take)
{
FileSystemTreeItemPresentationModel[] viewModels = _fileSystemTreeService.GetPathViewModels(string.Empty, skip, take, out var totalItems);
FileSystemTreeItemPresentationModel[] viewModels = GetPathViewModels(string.Empty, skip, take, out var totalItems);
PagedViewModel<FileSystemTreeItemPresentationModel> result = PagedViewModel(viewModels, totalItems);
return Task.FromResult<ActionResult<PagedViewModel<FileSystemTreeItemPresentationModel>>>(Ok(result));
@@ -37,14 +43,14 @@ public abstract class FileSystemTreeControllerBase : ManagementApiControllerBase
protected Task<ActionResult<PagedViewModel<FileSystemTreeItemPresentationModel>>> GetChildren(string path, int skip, int take)
{
FileSystemTreeItemPresentationModel[] viewModels = _fileSystemTreeService.GetPathViewModels(path, skip, take, out var totalItems);
FileSystemTreeItemPresentationModel[] viewModels = GetPathViewModels(path, skip, take, out var totalItems);
PagedViewModel<FileSystemTreeItemPresentationModel> result = PagedViewModel(viewModels, totalItems);
return Task.FromResult<ActionResult<PagedViewModel<FileSystemTreeItemPresentationModel>>>(Ok(result));
}
/// <summary>
/// Gets the sibling of the targeted item based on its path.
/// Gets the siblings of the targeted item based on its path.
/// </summary>
/// <param name="path">The path to the item.</param>
/// <param name="before">The amount of siblings you want to fetch from before the items position in the array.</param>
@@ -61,17 +67,19 @@ public abstract class FileSystemTreeControllerBase : ManagementApiControllerBase
protected virtual Task<ActionResult<IEnumerable<FileSystemTreeItemPresentationModel>>> GetAncestors(string path, bool includeSelf = true)
{
path = path.VirtualPathToSystemPath();
FileSystemTreeItemPresentationModel[] models = _fileSystemTreeService.GetAncestorModels(path, includeSelf);
FileSystemTreeItemPresentationModel[] models = GetAncestorModels(path, includeSelf);
return Task.FromResult<ActionResult<IEnumerable<FileSystemTreeItemPresentationModel>>>(Ok(models));
}
private PagedViewModel<FileSystemTreeItemPresentationModel> PagedViewModel(IEnumerable<FileSystemTreeItemPresentationModel> viewModels, long totalItems)
=> new() { Total = totalItems, Items = viewModels };
[Obsolete("Has been moved to FileSystemTreeServiceBase. Scheduled for removal in Umbraco 18.")]
[Obsolete("Has been moved to FileSystemTreeServiceBase. Scheduled for removal in Umbraco 19.")]
protected virtual FileSystemTreeItemPresentationModel[] GetAncestorModels(string path, bool includeSelf)
{
if (_useFileSystemTreeService)
{
return _fileSystemTreeService.GetAncestorModels(path, includeSelf);
}
var directories = path.Split(Path.DirectorySeparatorChar).Take(Range.EndAt(Index.FromEnd(1))).ToArray();
var result = directories
.Select((directory, index) => MapViewModel(string.Join(Path.DirectorySeparatorChar, directories.Take(index + 1)), directory, true))
@@ -86,28 +94,59 @@ public abstract class FileSystemTreeControllerBase : ManagementApiControllerBase
return result.ToArray();
}
[Obsolete("Has been moved to FileSystemTreeServiceBase. Scheduled for removal in Umbraco 18.")]
[Obsolete("Has been moved to FileSystemTreeServiceBase. Scheduled for removal in Umbraco 19.")]
protected virtual string[] GetDirectories(string path) => FileSystem
.GetDirectories(path)
.OrderBy(directory => directory)
.ToArray();
[Obsolete("Has been moved to FileSystemTreeServiceBase. Scheduled for removal in Umbraco 18.")]
[Obsolete("Has been moved to FileSystemTreeServiceBase. Scheduled for removal in Umbraco 19.")]
protected virtual string[] GetFiles(string path) => FileSystem
.GetFiles(path)
.OrderBy(file => file)
.ToArray();
[Obsolete("Has been moved to FileSystemTreeServiceBase. Scheduled for removal in Umbraco 18.")]
[Obsolete("Has been moved to FileSystemTreeServiceBase. Scheduled for removal in Umbraco 19.")]
protected virtual bool DirectoryHasChildren(string path)
=> FileSystem.GetFiles(path).Any() || FileSystem.GetDirectories(path).Any();
[Obsolete("Has been moved to FileSystemTreeServiceBase. Scheduled for removal in Umbraco 18.")]
[Obsolete("Has been moved to FileSystemTreeServiceBase. Scheduled for removal in Umbraco 19.")]
private string GetFileSystemItemName(bool isFolder, string itemPath) => isFolder
? Path.GetFileName(itemPath)
: FileSystem.GetFileName(itemPath);
[Obsolete("Has been moved to FileSystemTreeServiceBase. Scheduled for removal in Umbraco 18.")]
private FileSystemTreeItemPresentationModel[] GetPathViewModels(string path, int skip, int take, out long totalItems)
{
if (_useFileSystemTreeService)
{
return _fileSystemTreeService.GetPathViewModels(path, skip, take, out totalItems);
}
path = path.VirtualPathToSystemPath();
var allItems = GetDirectories(path)
.Select(directory => new { Path = directory, IsFolder = true })
.Union(GetFiles(path).Select(file => new { Path = file, IsFolder = false }))
.ToArray();
totalItems = allItems.Length;
FileSystemTreeItemPresentationModel ViewModel(string itemPath, bool isFolder)
=> MapViewModel(
itemPath,
GetFileSystemItemName(isFolder, itemPath),
isFolder);
return allItems
.Skip(skip)
.Take(take)
.Select(item => ViewModel(item.Path, item.IsFolder))
.ToArray();
}
private PagedViewModel<FileSystemTreeItemPresentationModel> PagedViewModel(IEnumerable<FileSystemTreeItemPresentationModel> viewModels, long totalItems)
=> new() { Total = totalItems, Items = viewModels };
[Obsolete("Has been moved to FileSystemTreeServiceBase. Scheduled for removal in Umbraco 19.")]
private FileSystemTreeItemPresentationModel MapViewModel(string path, string name, bool isFolder)
{
var parentPath = Path.GetDirectoryName(path);
@@ -1,11 +1,14 @@
using Asp.Versioning;
using Asp.Versioning;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.DependencyInjection;
using Umbraco.Cms.Api.Management.Controllers.UserGroup;
using Umbraco.Cms.Api.Management.Routing;
using Umbraco.Cms.Api.Management.ViewModels.User;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Core.Security;
using Umbraco.Cms.Core.Security.Authorization;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Services.OperationStatus;
@@ -25,11 +28,26 @@ public class UpdateUserGroupsUserController : UserGroupControllerBase
{
private readonly IAuthorizationService _authorizationService;
private readonly IUserGroupService _userGroupService;
private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor;
public UpdateUserGroupsUserController(IAuthorizationService authorizationService, IUserGroupService userGroupService)
[ActivatorUtilitiesConstructor]
public UpdateUserGroupsUserController(
IAuthorizationService authorizationService,
IUserGroupService userGroupService,
IBackOfficeSecurityAccessor backOfficeSecurityAccessor)
{
_authorizationService = authorizationService;
_userGroupService = userGroupService;
_backOfficeSecurityAccessor = backOfficeSecurityAccessor;
}
[Obsolete("Please use the constructor accepting all parameters. Scheduled for removal in Umbraco 19.")]
public UpdateUserGroupsUserController(IAuthorizationService authorizationService, IUserGroupService userGroupService)
: this(
authorizationService,
userGroupService,
StaticServiceProvider.Instance.GetRequiredService<IBackOfficeSecurityAccessor>())
{
}
[HttpPost("set-user-groups")]
@@ -51,7 +69,8 @@ public class UpdateUserGroupsUserController : UserGroupControllerBase
Attempt<UserGroupOperationStatus> result = await _userGroupService.UpdateUserGroupsOnUsersAsync(
requestModel.UserGroupIds.Select(x => x.Id).ToHashSet(),
requestModel.UserIds.Select(x => x.Id).ToHashSet());
requestModel.UserIds.Select(x => x.Id).ToHashSet(),
CurrentUserKey(_backOfficeSecurityAccessor));
return result.Success
? Ok()
@@ -1,5 +1,9 @@
using Umbraco.Cms.Api.Management.Factories;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Umbraco.Cms.Api.Common.Accessors;
using Umbraco.Cms.Api.Common.Rendering;
using Umbraco.Cms.Api.Management.Factories;
using Umbraco.Cms.Api.Management.Mapping.Webhook;
using Umbraco.Cms.Core.DeliveryApi;
using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Extensions;
@@ -12,6 +16,11 @@ internal static class WebhooksBuilderExtensions
builder.Services.AddUnique<IWebhookPresentationFactory, WebhookPresentationFactory>();
builder.AddMapDefinition<WebhookEventMapDefinition>();
// We have to use TryAdd here, as if they are registered by the delivery API, we don't want to register them
// Delivery API will also overwrite these IF it is enabled.
builder.Services.TryAddScoped<IOutputExpansionStrategy, ElementOnlyOutputExpansionStrategy>();
builder.Services.TryAddSingleton<IOutputExpansionStrategyAccessor, RequestContextOutputExpansionStrategyAccessor>();
return builder;
}
}
@@ -12,6 +12,7 @@ internal static class DocumentVariantStateHelper
culture,
content.Edited,
content.Published,
content.Trashed,
content.AvailableCultures,
content.EditedCultures ?? Enumerable.Empty<string>(),
content.PublishedCultures);
@@ -22,17 +23,23 @@ internal static class DocumentVariantStateHelper
culture,
content.Edited,
content.Published,
content.Trashed,
content.CultureNames.Keys,
content.EditedCultures,
content.PublishedCultures);
private static DocumentVariantState GetState(IEntity entity, string? culture, bool edited, bool published, IEnumerable<string> availableCultures, IEnumerable<string> editedCultures, IEnumerable<string> publishedCultures)
private static DocumentVariantState GetState(IEntity entity, string? culture, bool edited, bool published, bool trashed, IEnumerable<string> availableCultures, IEnumerable<string> editedCultures, IEnumerable<string> publishedCultures)
{
if (entity.Id <= 0 || (culture is not null && availableCultures.Contains(culture) is false))
{
return DocumentVariantState.NotCreated;
}
if (trashed)
{
return DocumentVariantState.Trashed;
}
var isDraft = published is false ||
(culture != null && publishedCultures.Contains(culture) is false);
if (isDraft)
+50 -6
View File
@@ -37322,6 +37322,9 @@
{
"$ref": "#/components/schemas/DocumentPropertyValuePermissionPresentationModel"
},
{
"$ref": "#/components/schemas/DocumentTypePermissionPresentationModel"
},
{
"$ref": "#/components/schemas/UnknownTypePermissionPresentationModel"
}
@@ -37608,6 +37611,9 @@
{
"$ref": "#/components/schemas/DocumentPropertyValuePermissionPresentationModel"
},
{
"$ref": "#/components/schemas/DocumentTypePermissionPresentationModel"
},
{
"$ref": "#/components/schemas/UnknownTypePermissionPresentationModel"
}
@@ -38444,7 +38450,8 @@
"type": "boolean"
},
"allowNonExistingSegmentsCreation": {
"type": "boolean"
"type": "boolean",
"deprecated": true
}
},
"additionalProperties": false
@@ -39088,6 +39095,36 @@
},
"additionalProperties": false
},
"DocumentTypePermissionPresentationModel": {
"required": [
"$type",
"documentTypeAlias",
"verbs"
],
"type": "object",
"properties": {
"$type": {
"type": "string"
},
"verbs": {
"uniqueItems": true,
"type": "array",
"items": {
"type": "string"
}
},
"documentTypeAlias": {
"type": "string"
}
},
"additionalProperties": false,
"discriminator": {
"propertyName": "$type",
"mapping": {
"DocumentTypePermissionPresentationModel": "#/components/schemas/DocumentTypePermissionPresentationModel"
}
}
},
"DocumentTypePropertyTypeContainerResponseModel": {
"required": [
"id",
@@ -39683,7 +39720,8 @@
"NotCreated",
"Draft",
"Published",
"PublishedPendingChanges"
"PublishedPendingChanges",
"Trashed"
],
"type": "string"
},
@@ -40178,7 +40216,7 @@
},
"actionParameters": {
"type": "object",
"additionalProperties": { },
"additionalProperties": {},
"nullable": true
}
},
@@ -40803,7 +40841,7 @@
},
"extensions": {
"type": "array",
"items": { }
"items": {}
}
},
"additionalProperties": false
@@ -44640,7 +44678,7 @@
"nullable": true
}
},
"additionalProperties": { }
"additionalProperties": {}
},
"ProblemDetailsBuilderModel": {
"type": "object",
@@ -47813,6 +47851,9 @@
{
"$ref": "#/components/schemas/DocumentPropertyValuePermissionPresentationModel"
},
{
"$ref": "#/components/schemas/DocumentTypePermissionPresentationModel"
},
{
"$ref": "#/components/schemas/UnknownTypePermissionPresentationModel"
}
@@ -48250,6 +48291,9 @@
{
"$ref": "#/components/schemas/DocumentPropertyValuePermissionPresentationModel"
},
{
"$ref": "#/components/schemas/DocumentTypePermissionPresentationModel"
},
{
"$ref": "#/components/schemas/UnknownTypePermissionPresentationModel"
}
@@ -48932,4 +48976,4 @@
}
}
}
}
}
@@ -75,9 +75,12 @@ public abstract class FileSystemTreeServiceBase : IFileSystemTreeService
public string[] GetFiles(string path) => FileSystem
.GetFiles(path)
.Where(FilterFile)
.OrderBy(file => file)
.ToArray();
protected virtual bool FilterFile(string file) => true;
public bool DirectoryHasChildren(string path)
=> FileSystem.GetFiles(path).Any() || FileSystem.GetDirectories(path).Any();
@@ -11,4 +11,6 @@ public class PartialViewTreeService : FileSystemTreeServiceBase, IPartialViewTre
public PartialViewTreeService(FileSystems fileSystems) =>
_partialViewFileSystem = fileSystems.PartialViewsFileSystem ??
throw new ArgumentException("Missing partial views file system", nameof(fileSystems));
protected override bool FilterFile(string file) => file.ToLowerInvariant().EndsWith(".cshtml");
}
@@ -11,4 +11,6 @@ public class ScriptTreeService : FileSystemTreeServiceBase, IScriptTreeService
public ScriptTreeService(FileSystems fileSystems) =>
_scriptFileSystem = fileSystems.ScriptsFileSystem ??
throw new ArgumentException("Missing partial views file system", nameof(fileSystems));
protected override bool FilterFile(string file) => file.ToLowerInvariant().EndsWith(".js");
}
@@ -10,5 +10,7 @@ public class StyleSheetTreeService : FileSystemTreeServiceBase, IStyleSheetTreeS
public StyleSheetTreeService(FileSystems fileSystems) =>
_scriptFileSystem = fileSystems.StylesheetsFileSystem ??
throw new ArgumentException("Missing partial views file system", nameof(fileSystems));
throw new ArgumentException("Missing stylesheets file system", nameof(fileSystems));
protected override bool FilterFile(string file) => file.ToLowerInvariant().EndsWith(".css");
}
@@ -24,4 +24,9 @@ public enum DocumentVariantState
/// The item is published and there are pending changes
/// </summary>
PublishedPendingChanges = 4,
/// <summary>
/// The item is in the recycle bin
/// </summary>
Trashed = 5,
}
@@ -0,0 +1,31 @@
using System.ComponentModel;
namespace Umbraco.Cms.Core.Configuration.Models;
/// <summary>
/// Typed configuration options for back-office token cookie settings.
/// </summary>
[UmbracoOptions(Constants.Configuration.ConfigBackOfficeTokenCookie)]
[Obsolete("This will be replaced with a different authentication scheme. Scheduled for removal in Umbraco 18.")]
public class BackOfficeTokenCookieSettings
{
private const bool StaticEnabled = false;
private const string StaticSameSite = "Strict";
/// <summary>
/// Gets or sets a value indicating whether to enable access and refresh tokens in cookies.
/// </summary>
[DefaultValue(StaticEnabled)]
[Obsolete("This is only configurable in Umbraco 16. Scheduled for removal in Umbraco 17.")]
public bool Enabled { get; set; } = StaticEnabled;
/// <summary>
/// Gets or sets a value indicating whether the cookie SameSite configuration.
/// </summary>
/// <remarks>
/// Valid values are "Unspecified", "None", "Lax" and "Strict" (default).
/// </remarks>
[DefaultValue(StaticSameSite)]
public string SameSite { get; set; } = StaticSameSite;
}
@@ -64,6 +64,7 @@ public static partial class Constants
public const string ConfigWebhook = ConfigPrefix + "Webhook";
public const string ConfigWebhookPayloadType = ConfigWebhook + ":PayloadType";
public const string ConfigCache = ConfigPrefix + "Cache";
public const string ConfigBackOfficeTokenCookie = ConfigSecurity + ":BackOfficeTokenCookie";
public static class NamedOptions
{
@@ -86,7 +86,8 @@ public static partial class UmbracoBuilderExtensions
.AddUmbracoOptions<HelpPageSettings>()
.AddUmbracoOptions<DataTypesSettings>()
.AddUmbracoOptions<WebhookSettings>()
.AddUmbracoOptions<CacheSettings>();
.AddUmbracoOptions<CacheSettings>()
.AddUmbracoOptions<BackOfficeTokenCookieSettings>();
// Configure connection string and ensure it's updated when the configuration changes
builder.Services.AddSingleton<IConfigureOptions<ConnectionStrings>, ConfigureConnectionStrings>();
@@ -81,46 +81,23 @@ public class UserEditorAuthorizationHelper
return Attempt<string?>.Succeed();
}
// d) a non-admin user can remove any groups but can only add groups they themselves belong to
if (userGroupAliases != null)
{
var savingGroupAliases = userGroupAliases.ToArray();
var existingGroupAliases = savingUser == null
IEnumerable<string> requestedGroupAliases = userGroupAliases.ToArray();
IEnumerable<string> existingGroupAliases = savingUser == null
? []
: savingUser.Groups.Select(x => x.Alias).ToArray();
: savingUser.Groups.Select(x => x.Alias);
IEnumerable<string> performingUserGroupAliases = currentUser?.Groups.Select(x => x.Alias) ?? Enumerable.Empty<string>();
IEnumerable<string> addedGroupAliases = savingGroupAliases.Except(existingGroupAliases);
IReadOnlyList<string> unauthorized = UserGroupAssignmentAuthorization
.GetUnauthorizedGroupAssignments(performingUserGroupAliases, requestedGroupAliases, existingGroupAliases);
// As we know the current user is not admin, it is only allowed to use groups that the user do have themselves.
var savingGroupAliasesNotAllowed = addedGroupAliases
.Except(currentUser?.Groups.Select(x => x.Alias) ?? Enumerable.Empty<string>()).ToArray();
if (savingGroupAliasesNotAllowed.Any())
if (unauthorized.Count > 0)
{
return Attempt.Fail("Cannot assign the group(s) '" + string.Join(", ", savingGroupAliasesNotAllowed) +
return Attempt.Fail("Cannot assign the group(s) '" + string.Join(", ", unauthorized) +
"', the current user is not part of them or admin");
}
// only validate any groups that have changed.
// a non-admin user can remove groups and add groups that they have access to
// but they cannot add a group that they do not have access to or that grants them
// path or section access that they don't have access to.
var newGroups = savingUser == null
? savingGroupAliases
: savingGroupAliases.Except(savingUser.Groups.Select(x => x.Alias)).ToArray();
var userGroupsChanged = savingUser != null && newGroups.Length > 0;
if (userGroupsChanged)
{
// d) A user cannot assign a group to another user that they do not belong to
var currentUserGroups = currentUser?.Groups.Select(x => x.Alias).ToArray();
foreach (var group in newGroups)
{
if (currentUserGroups?.Contains(group) == false)
{
return Attempt.Fail("Cannot assign the group " + group + ", the current user is not a member");
}
}
}
}
return Attempt<string?>.Succeed();
+1 -1
View File
@@ -12,7 +12,7 @@ public class X : OEmbedProviderBase
{
}
public override string ApiEndpoint => "http://publish.twitter.com/oembed";
public override string ApiEndpoint => "https://publish.x.com/oembed";
public override string[] UrlSchemeRegex => new[] { @"(https?:\/\/(www\.)?)(twitter|x)\.com\/.*\/status\/.*" };
@@ -23,4 +23,11 @@ public class RichTextBlockValue : BlockValue<RichTextBlockLayoutItem>
/// <inheritdoc />
[JsonIgnore]
public override string PropertyEditorAlias => Constants.PropertyEditors.Aliases.RichText;
/// <inheritdoc />
#pragma warning disable CS0672 // Member overrides obsolete member
#pragma warning disable CS0618 // Type or member is obsolete
public override bool SupportsBlockLayoutAlias(string alias) => base.SupportsBlockLayoutAlias(alias) || alias.Equals("Umbraco.TinyMCE");
#pragma warning restore CS0618 // Type or member is obsolete
#pragma warning restore CS0672 // Member overrides obsolete member
}
@@ -53,6 +53,12 @@ public interface IPublishedContentTypeFactory
/// </summary>
PublishedDataType GetDataType(int id);
/// <summary>
/// Clears the internal data type cache.
/// </summary>
void ClearDataTypeCache()
{ }
/// <summary>
/// Notifies the factory of datatype changes.
/// </summary>
@@ -65,6 +65,22 @@ public class PublishedContentTypeFactory : IPublishedContentTypeFactory
return dataType;
}
/// <inheritdoc />
public void ClearDataTypeCache()
{
if (_publishedDataTypes is null)
{
// Not initialized yet, so skip and avoid lock
return;
}
lock (_publishedDataTypesLocker)
{
// Clear cache (and let it lazy initialize again later)
_publishedDataTypes = null;
}
}
/// <inheritdoc />
public void NotifyDataTypeChanges(params int[] ids)
{
@@ -24,6 +24,7 @@ public sealed class ContentPublishedNotification : EnumerableObjectNotification<
public ContentPublishedNotification(IEnumerable<IContent> target, EventMessages messages, bool includeDescendants)
: base(target, messages) => IncludeDescendants = includeDescendants;
/// <summary>
/// Gets a enumeration of <see cref="IContent"/> which are being published.
/// </summary>
@@ -3,12 +3,14 @@ using System.Globalization;
using System.Runtime.Serialization;
using System.Xml.Linq;
using Microsoft.Extensions.Logging;
using Umbraco.Cms.Core.Cache;
using Umbraco.Cms.Core.IO;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.Editors;
using Umbraco.Cms.Core.Models.Validation;
using Umbraco.Cms.Core.PropertyEditors.Validators;
using Umbraco.Cms.Core.Serialization;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Strings;
using Umbraco.Extensions;
@@ -20,6 +22,9 @@ namespace Umbraco.Cms.Core.PropertyEditors;
[DataContract]
public class DataValueEditor : IDataValueEditor
{
private const string ContentCacheKeyFormat = nameof(DataValueEditor) + "_Content_{0}";
private const string MediaCacheKeyFormat = nameof(DataValueEditor) + "_Media_{0}";
private readonly IJsonSerializer? _jsonSerializer;
private readonly IShortStringHelper _shortStringHelper;
@@ -415,4 +420,155 @@ public class DataValueEditor : IDataValueEditor
return value.TryConvertTo(valueType);
}
/// <summary>
/// Retrieves a <see cref="IContent"/> instance by its unique identifier, using the provided request cache to avoid redundant
/// lookups within the same request.
/// </summary>
/// <remarks>
/// This method caches content lookups for the duration of the current request to improve performance when the same content
/// item may be accessed multiple times. This is particularly useful in scenarios involving multiple languages or blocks.
/// </remarks>
/// <param name="key">The unique identifier of the content item to retrieve.</param>
/// <param name="requestCache">The request-scoped cache used to store and retrieve content items for the duration of the current request.</param>
/// <param name="contentService">The content service used to fetch the content item if it is not found in the cache.</param>
/// <returns>The <see cref="IContent"/> instance corresponding to the specified key, or null if no such content item exists.</returns>
[Obsolete("This method is available for support of request caching retrieved entities in derived property value editors. " +
"The intention is to supersede this with lazy loaded read locks, which will make this unnecessary. " +
"Scheduled for removal in Umbraco 19.")]
protected static IContent? GetAndCacheContentById(Guid key, IRequestCache requestCache, IContentService contentService)
{
if (requestCache.IsAvailable is false)
{
return contentService.GetById(key);
}
var cacheKey = string.Format(ContentCacheKeyFormat, key);
IContent? content = requestCache.GetCacheItem<IContent?>(cacheKey);
if (content is null)
{
content = contentService.GetById(key);
if (content is not null)
{
requestCache.Set(cacheKey, content);
}
}
return content;
}
/// <summary>
/// Adds the specified <see cref="IContent"/> item to the request cache using its unique key.
/// </summary>
/// <param name="content">The content item to cache.</param>
/// <param name="requestCache">The request cache in which to store the content item.</param>
[Obsolete("This method is available for support of request caching retrieved entities in derived property value editors. " +
"The intention is to supersede this with lazy loaded read locks, which will make this unnecessary. " +
"Scheduled for removal in Umbraco 19.")]
protected static void CacheContentById(IContent content, IRequestCache requestCache)
{
if (requestCache.IsAvailable is false)
{
return;
}
var cacheKey = string.Format(ContentCacheKeyFormat, content.Key);
requestCache.Set(cacheKey, content);
}
/// <summary>
/// Retrieves a <see cref="IMedia"/> instance by its unique identifier, using the provided request cache to avoid redundant
/// lookups within the same request.
/// </summary>
/// <remarks>
/// This method caches media lookups for the duration of the current request to improve performance when the same media
/// item may be accessed multiple times. This is particularly useful in scenarios involving multiple languages or blocks.
/// </remarks>
/// <param name="key">The unique identifier of the media item to retrieve.</param>
/// <param name="requestCache">The request-scoped cache used to store and retrieve media items for the duration of the current request.</param>
/// <param name="mediaService">The media service used to fetch the media item if it is not found in the cache.</param>
/// <returns>The <see cref="IMedia"/> instance corresponding to the specified key, or null if no such media item exists.</returns>
[Obsolete("This method is available for support of request caching retrieved entities in derived property value editors. " +
"The intention is to supersede this with lazy loaded read locks, which will make this unnecessary. " +
"Scheduled for removal in Umbraco 19.")]
protected static IMedia? GetAndCacheMediaById(Guid key, IRequestCache requestCache, IMediaService mediaService)
{
if (requestCache.IsAvailable is false)
{
return mediaService.GetById(key);
}
var cacheKey = string.Format(MediaCacheKeyFormat, key);
IMedia? media = requestCache.GetCacheItem<IMedia?>(cacheKey);
if (media is null)
{
media = mediaService.GetById(key);
if (media is not null)
{
requestCache.Set(cacheKey, media);
}
}
return media;
}
/// <summary>
/// Adds the specified <see cref="IMedia"/> item to the request cache using its unique key.
/// </summary>
/// <param name="media">The media item to cache.</param>
/// <param name="requestCache">The request cache in which to store the media item.</param>
[Obsolete("This method is available for support of request caching retrieved entities in derived property value editors. " +
"The intention is to supersede this with lazy loaded read locks, which will make this unnecessary. " +
"Scheduled for removal in Umbraco 19.")]
protected static void CacheMediaById(IMedia media, IRequestCache requestCache)
{
if (requestCache.IsAvailable is false)
{
return;
}
var cacheKey = string.Format(MediaCacheKeyFormat, media.Key);
requestCache.Set(cacheKey, media);
}
/// <summary>
/// Determines whether the content item identified by the specified key is present in the request cache.
/// </summary>
/// <param name="key">The unique identifier for the content item to check for in the cache.</param>
/// <param name="requestCache">The request cache in which to look for the content item.</param>
/// <returns>true if the content item is already cached in the request cache; otherwise, false.</returns>
[Obsolete("This method is available for support of request caching retrieved entities in derived property value editors. " +
"The intention is to supersede this with lazy loaded read locks, which will make this unnecessary. " +
"Scheduled for removal in Umbraco 19.")]
protected static bool IsContentAlreadyCached(Guid key, IRequestCache requestCache)
{
if (requestCache.IsAvailable is false)
{
return false;
}
var cacheKey = string.Format(ContentCacheKeyFormat, key);
return requestCache.GetCacheItem<IContent?>(cacheKey) is not null;
}
/// <summary>
/// Determines whether the media item identified by the specified key is present in the request cache.
/// </summary>
/// <param name="key">The unique identifier for the media item to check for in the cache.</param>
/// <param name="requestCache">The request cache in which to look for the media item.</param>
/// <returns>true if the media item is already cached in the request cache; otherwise, false.</returns>
[Obsolete("This method is available for support of request caching retrieved entities in derived property value editors. " +
"The intention is to supersede this with lazy loaded read locks, which will make this unnecessary. " +
"Scheduled for removal in Umbraco 19.")]
protected static bool IsMediaAlreadyCached(Guid key, IRequestCache requestCache)
{
if (requestCache.IsAvailable is false)
{
return false;
}
var cacheKey = string.Format(MediaCacheKeyFormat, key);
return requestCache.GetCacheItem<IMedia?>(cacheKey) is not null;
}
}
@@ -0,0 +1,19 @@
namespace Umbraco.Cms.Core.PropertyEditors;
/// <summary>
/// Optionally implemented by property editors, this defines a contract for caching entities that are referenced in block values.
/// </summary>
[Obsolete("This interface is available for support of request caching retrieved entities in property value editors that implement it. " +
"The intention is to supersede this with lazy loaded read locks, which will make this unnecessary. " +
"Scheduled for removal in Umbraco 19.")]
public interface ICacheReferencedEntities
{
/// <summary>
/// Caches the entities referenced by the provided block data values.
/// </summary>
/// <param name="values">An enumerable collection of block values that may contain the entities to be cached.</param>
[Obsolete("This method is available for support of request caching retrieved entities in derived property value editors. " +
"The intention is to supersede this with lazy loaded read locks, which will make this unnecessary. " +
"Scheduled for removal in Umbraco 19.")]
void CacheReferencedEntities(IEnumerable<object> values);
}
@@ -0,0 +1,34 @@
namespace Umbraco.Cms.Core.Security;
/// <summary>
/// Shared authorization logic for user group assignment.
/// </summary>
public static class UserGroupAssignmentAuthorization
{
/// <summary>
/// Returns the group aliases that the performing user is not authorized to assign.
/// </summary>
/// <param name="performingUserGroupAliases">The group aliases the performing user belongs to.</param>
/// <param name="requestedGroupAliases">The group aliases being assigned to the target user.</param>
/// <param name="existingGroupAliases">The group aliases the target user currently belongs to.</param>
/// <returns>
/// Group aliases that are being added but the performing user does not belong to.
/// An empty collection means the assignment is authorized.
/// </returns>
/// <remarks>
/// Non-admin users can remove any groups but can only add groups they themselves belong to.
/// Callers should check for admin status before calling this method, as admins bypass this check.
/// </remarks>
public static IReadOnlyList<string> GetUnauthorizedGroupAssignments(
IEnumerable<string> performingUserGroupAliases,
IEnumerable<string> requestedGroupAliases,
IEnumerable<string> existingGroupAliases)
{
var performingGroups = performingUserGroupAliases.ToHashSet(StringComparer.InvariantCultureIgnoreCase);
var existingGroups = existingGroupAliases.ToHashSet(StringComparer.InvariantCultureIgnoreCase);
return requestedGroupAliases
.Where(alias => existingGroups.Contains(alias) is false && performingGroups.Contains(alias) is false)
.ToArray();
}
}
@@ -8,6 +8,7 @@ using Umbraco.Cms.Core.Models.ContentEditing;
using Umbraco.Cms.Core.Models.ContentPublishing;
using Umbraco.Cms.Core.Scoping;
using Umbraco.Cms.Core.Services.OperationStatus;
using Umbraco.Cms.Core.Web;
using Umbraco.Extensions;
namespace Umbraco.Cms.Core.Services;
@@ -26,6 +27,7 @@ internal sealed class ContentPublishingService : IContentPublishingService
private readonly IRelationService _relationService;
private readonly ILogger<ContentPublishingService> _logger;
private readonly ILongRunningOperationService _longRunningOperationService;
private readonly IUmbracoContextFactory _umbracoContextFactory;
public ContentPublishingService(
ICoreScopeProvider coreScopeProvider,
@@ -37,7 +39,8 @@ internal sealed class ContentPublishingService : IContentPublishingService
IOptionsMonitor<ContentSettings> optionsMonitor,
IRelationService relationService,
ILogger<ContentPublishingService> logger,
ILongRunningOperationService longRunningOperationService)
ILongRunningOperationService longRunningOperationService,
IUmbracoContextFactory umbracoContextFactory)
{
_coreScopeProvider = coreScopeProvider;
_contentService = contentService;
@@ -53,6 +56,7 @@ internal sealed class ContentPublishingService : IContentPublishingService
{
_contentSettings = contentSettings;
});
_umbracoContextFactory = umbracoContextFactory;
}
/// <inheritdoc />
@@ -290,7 +294,7 @@ internal sealed class ContentPublishingService : IContentPublishingService
return MapInternalPublishingAttempt(minimalAttempt);
}
_logger.LogInformation("Starting async background thread for publishing branch.");
_logger.LogDebug("Starting long running operation for publishing branch {Key} on background thread.", key);
Attempt<Guid, LongRunningOperationEnqueueStatus> enqueueAttempt = await _longRunningOperationService.RunAsync(
PublishBranchOperationType,
async _ => await PerformPublishBranchAsync(key, cultures, publishBranchFilter, userKey, returnContent: false),
@@ -324,6 +328,10 @@ internal sealed class ContentPublishingService : IContentPublishingService
Guid userKey,
bool returnContent)
{
// Ensure we have an UmbracoContext in case running on a background thread so operations that run in the published notification handlers
// have access to this (e.g. webhooks).
using UmbracoContextReference umbracoContextReference = _umbracoContextFactory.EnsureUmbracoContext();
using ICoreScope scope = _coreScopeProvider.CreateCoreScope();
IContent? content = _contentService.GetById(key);
if (content is null)
+1 -1
View File
@@ -2207,7 +2207,7 @@ public class ContentService : RepositoryService, IContentService
variesByCulture ? culturesPublished.IsCollectionEmpty() ? null : culturesPublished : ["*"],
null,
eventMessages));
scope.Notifications.Publish(new ContentPublishedNotification(publishedDocuments, eventMessages).WithState(notificationState));
scope.Notifications.Publish(new ContentPublishedNotification(publishedDocuments, eventMessages, true).WithState(notificationState));
scope.Complete();
}
@@ -175,7 +175,7 @@ internal sealed class MediaTypeEditingService : ContentTypeEditingServiceBase<IM
continue;
}
allowedFileExtensionsByMediaType[mediaType] = fileUploadConfiguration.FileExtensions;
allowedFileExtensionsByMediaType[mediaType] = fileUploadConfiguration.FileExtensions ?? []; // Although we never expect null here, legacy data type configuration did allow it.
}
return allowedFileExtensionsByMediaType;
@@ -2,7 +2,22 @@ using Umbraco.Cms.Core.Services.OperationStatus;
namespace Umbraco.Cms.Core.Services;
/// <summary>
/// Defines a service for asynchronously retrieving embeddable HTML markup for a specified resource using the oEmbed
/// protocol.
/// </summary>
public interface IOEmbedService
{
/// <summary>
/// Asynchronously retrieves the embeddable HTML markup for the specified resource.
/// </summary>
/// <remarks>The returned markup is suitable for embedding in web pages. The width and height parameters
/// may be ignored by some providers depending on their capabilities.</remarks>
/// <param name="url">The URI of the resource to retrieve markup for. Must be a valid, absolute URI.</param>
/// <param name="width">The optional maximum width, in pixels, for the embedded content. If null, the default width is used.</param>
/// <param name="height">The optional maximum height, in pixels, for the embedded content. If null, the default height is used.</param>
/// <param name="cancellationToken">A token to monitor for cancellation requests. The operation is canceled if the token is triggered.</param>
/// <returns>A task that represents the asynchronous operation. The result contains an Attempt with the HTML markup if
/// successful, or an oEmbed operation status indicating the reason for failure.</returns>
Task<Attempt<string, OEmbedOperationStatus>> GetMarkupAsync(Uri url, int? width, int? height, CancellationToken cancellationToken);
}
+18 -1
View File
@@ -1,4 +1,4 @@
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.Membership;
using Umbraco.Cms.Core.Services.OperationStatus;
@@ -107,8 +107,25 @@ public interface IUserGroupService
/// <param name="userGroupKeys">The user groups the users should be part of.</param>
/// <param name="userKeys">The user whose groups we want to alter.</param>
/// <returns>An attempt indicating if the operation was a success as well as a more detailed <see cref="UserGroupOperationStatus"/>.</returns>
[Obsolete("Please use the overload accepting all parameters. Scheduled for removal in Umbraco 19.")]
Task<Attempt<UserGroupOperationStatus>> UpdateUserGroupsOnUsersAsync(ISet<Guid> userGroupKeys, ISet<Guid> userKeys);
/// <summary>
/// Updates the users to have the groups specified, with authorization checks based on the performing user.
/// </summary>
/// <param name="userGroupKeys">The user groups the users should be part of.</param>
/// <param name="userKeys">The user whose groups we want to alter.</param>
/// <param name="performingUserKey">The key of the user performing the operation.</param>
/// <returns>An attempt indicating if the operation was a success as well as a more detailed <see cref="UserGroupOperationStatus"/>.</returns>
/// <remarks>
/// Non-admin users can only add groups they themselves belong to. Removing groups is always allowed.
/// </remarks>
// TODO (V18): Remove default implementation.
Task<Attempt<UserGroupOperationStatus>> UpdateUserGroupsOnUsersAsync(ISet<Guid> userGroupKeys, ISet<Guid> userKeys, Guid performingUserKey)
#pragma warning disable CS0618 // Type or member is obsolete
=> UpdateUserGroupsOnUsersAsync(userGroupKeys, userKeys);
#pragma warning restore CS0618 // Type or member is obsolete
Task<Attempt<UserGroupOperationStatus>> AddUsersToUserGroupAsync(UsersToUserGroupManipulationModel addUsersModel, Guid performingUserKey);
Task<Attempt<UserGroupOperationStatus>> RemoveUsersFromUserGroupAsync(UsersToUserGroupManipulationModel removeUsersModel, Guid performingUserKey);
}
+11 -3
View File
@@ -6,22 +6,30 @@ using Umbraco.Cms.Core.Services.OperationStatus;
namespace Umbraco.Cms.Core.Services;
/// <summary>
/// Implements <see cref="IOEmbedService"/> for retrieving embeddable HTML markup using the oEmbed protocol.
/// </summary>
public class OEmbedService : IOEmbedService
{
private readonly EmbedProvidersCollection _embedProvidersCollection;
private readonly ILogger<OEmbedService> _logger;
/// <summary>
/// Initializes a new instance of the <see cref="OEmbedService"/> class.
/// </summary>
public OEmbedService(EmbedProvidersCollection embedProvidersCollection, ILogger<OEmbedService> logger)
{
_embedProvidersCollection = embedProvidersCollection;
_logger = logger;
}
/// <inheritdoc/>
public async Task<Attempt<string, OEmbedOperationStatus>> GetMarkupAsync(Uri url, int? maxWidth, int? maxHeight, CancellationToken cancellationToken)
{
// Find the first provider that supports the URL
IEmbedProvider? matchedProvider = _embedProvidersCollection
.FirstOrDefault(provider => provider.UrlSchemeRegex.Any(regex=>new Regex(regex, RegexOptions.IgnoreCase).IsMatch(url.OriginalString)));
.FirstOrDefault(provider => provider.UrlSchemeRegex
.Any(regex => new Regex(regex, RegexOptions.IgnoreCase).IsMatch(url.OriginalString)));
if (matchedProvider is null)
{
@@ -39,8 +47,8 @@ public class OEmbedService : IOEmbedService
}
catch (Exception e)
{
_logger.LogError(e, "Unexpected exception happened while trying to get oembed markup. Provider: {Provider}",matchedProvider.GetType().Name);
Attempt.FailWithStatus(OEmbedOperationStatus.UnexpectedException, string.Empty, e);
_logger.LogError(e, "Unexpected exception happened while trying to get oEmbed markup. Provider: {Provider}", matchedProvider.GetType().Name);
return Attempt.FailWithStatus(OEmbedOperationStatus.UnexpectedException, string.Empty, e);
}
return Attempt.FailWithStatus(OEmbedOperationStatus.ProviderReturnedInvalidResult, string.Empty);
+51 -1
View File
@@ -8,6 +8,7 @@ using Umbraco.Cms.Core.Persistence;
using Umbraco.Cms.Core.Persistence.Querying;
using Umbraco.Cms.Core.Persistence.Repositories;
using Umbraco.Cms.Core.Scoping;
using Umbraco.Cms.Core.Security;
using Umbraco.Cms.Core.Services.AuthorizationStatus;
using Umbraco.Cms.Core.Services.OperationStatus;
using Umbraco.Extensions;
@@ -210,9 +211,24 @@ internal sealed class UserGroupService : RepositoryService, IUserGroupService
return Attempt.Succeed(UserGroupOperationStatus.Success);
}
public async Task<Attempt<UserGroupOperationStatus>> UpdateUserGroupsOnUsersAsync(
// TODO (V19): Collapse the following three methods into a single one, once the obsolete overload
// of UpdateUserGroupsOnUsersAsync is removed from the interface.
public Task<Attempt<UserGroupOperationStatus>> UpdateUserGroupsOnUsersAsync(
ISet<Guid> userGroupKeys,
ISet<Guid> userKeys)
=> UpdateUserGroupsOnUsersInternalAsync(userGroupKeys, userKeys, performingUserKey: null);
public Task<Attempt<UserGroupOperationStatus>> UpdateUserGroupsOnUsersAsync(
ISet<Guid> userGroupKeys,
ISet<Guid> userKeys,
Guid performingUserKey)
=> UpdateUserGroupsOnUsersInternalAsync(userGroupKeys, userKeys, performingUserKey);
private async Task<Attempt<UserGroupOperationStatus>> UpdateUserGroupsOnUsersInternalAsync(
ISet<Guid> userGroupKeys,
ISet<Guid> userKeys,
Guid? performingUserKey)
{
using ICoreScope scope = ScopeProvider.CreateCoreScope();
@@ -222,6 +238,40 @@ internal sealed class UserGroupService : RepositoryService, IUserGroupService
.Select(x => x.ToReadOnlyGroup())
.ToArray();
// Authorize the performing user if provided.
if (performingUserKey.HasValue)
{
IUser? performingUser = await _userService.GetAsync(performingUserKey.Value);
if (performingUser is null)
{
scope.Complete();
return Attempt.Fail(UserGroupOperationStatus.MissingUser);
}
if (performingUser.IsAdmin() is false)
{
string[] performingUserGroupAliases = performingUser.Groups.Select(g => g.Alias).ToArray();
string[] requestedGroupAliases = userGroups.Select(g => g.Alias).ToArray();
foreach (IUser user in users)
{
IEnumerable<string> existingGroupAliases = user.Groups.Select(g => g.Alias);
IReadOnlyList<string> unauthorized = UserGroupAssignmentAuthorization
.GetUnauthorizedGroupAssignments(performingUserGroupAliases, requestedGroupAliases, existingGroupAliases);
if (unauthorized.Count > 0)
{
_logger.LogInformation(
"The performing user is not allowed to assign user group(s) '{GroupAliases}' because they do not belong to them.",
string.Join(", ", unauthorized));
scope.Complete();
return Attempt.Fail(UserGroupOperationStatus.Unauthorized);
}
}
}
}
// This means that we're potentially de-admining a user, which might cause the admin group to be empty.
if (userGroupKeys.Contains(Constants.Security.AdminGroupKey) is false)
{
@@ -10,7 +10,7 @@ using Umbraco.Cms.Core.Configuration.Models;
namespace Umbraco.Cms.Infrastructure.Examine.DependencyInjection;
/// <summary>
/// Configures the index options to construct the Examine indexes
/// Configures the index options to construct the Examine indexes.
/// </summary>
public sealed class ConfigureIndexOptions : IConfigureNamedOptions<LuceneDirectoryIndexOptions>
{
@@ -18,6 +18,9 @@ public sealed class ConfigureIndexOptions : IConfigureNamedOptions<LuceneDirecto
private readonly IUmbracoIndexConfig _umbracoIndexConfig;
private readonly IDeliveryApiContentIndexFieldDefinitionBuilder _deliveryApiContentIndexFieldDefinitionBuilder;
/// <summary>
/// Initializes a new instance of the <see cref="ConfigureIndexOptions"/> class.
/// </summary>
public ConfigureIndexOptions(
IUmbracoIndexConfig umbracoIndexConfig,
IOptions<IndexCreatorSettings> settings,
@@ -28,24 +31,27 @@ public sealed class ConfigureIndexOptions : IConfigureNamedOptions<LuceneDirecto
_deliveryApiContentIndexFieldDefinitionBuilder = deliveryApiContentIndexFieldDefinitionBuilder;
}
/// <inheritdoc/>
public void Configure(string? name, LuceneDirectoryIndexOptions options)
{
// When creating FieldDefinitions with Umbraco defaults, pass in any already defined to avoid overwriting
// those added via a package or custom code.
switch (name)
{
case Constants.UmbracoIndexes.InternalIndexName:
options.Analyzer = new CultureInvariantWhitespaceAnalyzer();
options.Validator = _umbracoIndexConfig.GetContentValueSetValidator();
options.FieldDefinitions = new UmbracoFieldDefinitionCollection();
options.FieldDefinitions = new UmbracoFieldDefinitionCollection(options.FieldDefinitions);
break;
case Constants.UmbracoIndexes.ExternalIndexName:
options.Analyzer = new StandardAnalyzer(LuceneInfo.CurrentVersion);
options.Validator = _umbracoIndexConfig.GetPublishedContentValueSetValidator();
options.FieldDefinitions = new UmbracoFieldDefinitionCollection();
options.FieldDefinitions = new UmbracoFieldDefinitionCollection(options.FieldDefinitions);
break;
case Constants.UmbracoIndexes.MembersIndexName:
options.Analyzer = new CultureInvariantWhitespaceAnalyzer();
options.Validator = _umbracoIndexConfig.GetMemberValueSetValidator();
options.FieldDefinitions = new UmbracoFieldDefinitionCollection();
options.FieldDefinitions = new UmbracoFieldDefinitionCollection(options.FieldDefinitions);
break;
case Constants.UmbracoIndexes.DeliveryApiContentIndexName:
options.Analyzer = new StandardAnalyzer(LuceneInfo.CurrentVersion);
@@ -64,6 +70,7 @@ public sealed class ConfigureIndexOptions : IConfigureNamedOptions<LuceneDirecto
}
}
/// <inheritdoc/>
public void Configure(LuceneDirectoryIndexOptions options)
=> throw new NotImplementedException("This is never called and is just part of the interface");
}
@@ -20,6 +20,7 @@ public static partial class UmbracoBuilderExtensions
// Add post migration notification handlers
builder.AddNotificationHandler<UmbracoPlanExecutedNotification, ClearCsrfCookieHandler>();
return builder;
}
}
@@ -30,11 +30,29 @@ public class UmbracoFieldDefinitionCollection : FieldDefinitionCollection
new(UmbracoExamineFieldNames.VariesByCultureFieldName, FieldDefinitionTypes.Raw),
};
/// <summary>
/// Initializes a new instance of the <see cref="UmbracoFieldDefinitionCollection"/> class containing
/// the default Umbraco field definitions.
/// </summary>
public UmbracoFieldDefinitionCollection()
: base(UmbracoIndexFieldDefinitions)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="UmbracoFieldDefinitionCollection"/> class containing the containing
/// the default Umbraco field definitions, augmented or overridden by the provided definitions.
/// </summary>
/// <param name="definitions">Existing collection of field definitions.</param>
public UmbracoFieldDefinitionCollection(FieldDefinitionCollection definitions)
: base(UmbracoIndexFieldDefinitions)
{
foreach (FieldDefinition definition in definitions)
{
AddOrUpdate(definition);
}
}
/// <summary>
/// Overridden to dynamically add field definitions for culture variations
/// </summary>
@@ -1,6 +1,8 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Cache;
using Umbraco.Cms.Core.Configuration;
using Umbraco.Cms.Core.Configuration.Models;
using Umbraco.Cms.Core.DependencyInjection;
@@ -28,7 +30,10 @@ public class UnattendedUpgrader : INotificationAsyncHandler<RuntimeUnattendedUpg
private readonly IRuntimeState _runtimeState;
private readonly IUmbracoVersion _umbracoVersion;
private readonly UnattendedSettings _unattendedSettings;
private readonly DistributedCache _distributedCache;
private readonly ILogger<UnattendedUpgrader> _logger;
[Obsolete("Please use the constructor taking all parameters. Scheduled for removal in Umbraco 19.")]
public UnattendedUpgrader(
IProfilingLogger profilingLogger,
IUmbracoVersion umbracoVersion,
@@ -36,13 +41,36 @@ public class UnattendedUpgrader : INotificationAsyncHandler<RuntimeUnattendedUpg
IRuntimeState runtimeState,
PackageMigrationRunner packageMigrationRunner,
IOptions<UnattendedSettings> unattendedSettings)
: this(
profilingLogger,
umbracoVersion,
databaseBuilder,
runtimeState,
packageMigrationRunner,
unattendedSettings,
StaticServiceProvider.Instance.GetRequiredService<DistributedCache>(),
StaticServiceProvider.Instance.GetRequiredService<ILogger<UnattendedUpgrader>>())
{
_profilingLogger = profilingLogger ?? throw new ArgumentNullException(nameof(profilingLogger));
_umbracoVersion = umbracoVersion ?? throw new ArgumentNullException(nameof(umbracoVersion));
_databaseBuilder = databaseBuilder ?? throw new ArgumentNullException(nameof(databaseBuilder));
_runtimeState = runtimeState ?? throw new ArgumentNullException(nameof(runtimeState));
}
public UnattendedUpgrader(
IProfilingLogger profilingLogger,
IUmbracoVersion umbracoVersion,
DatabaseBuilder databaseBuilder,
IRuntimeState runtimeState,
PackageMigrationRunner packageMigrationRunner,
IOptions<UnattendedSettings> unattendedSettings,
DistributedCache distributedCache,
ILogger<UnattendedUpgrader> logger)
{
_profilingLogger = profilingLogger;
_umbracoVersion = umbracoVersion;
_databaseBuilder = databaseBuilder;
_runtimeState = runtimeState;
_packageMigrationRunner = packageMigrationRunner;
_unattendedSettings = unattendedSettings.Value;
_distributedCache = distributedCache;
_logger = logger;
}
public async Task HandleAsync(RuntimeUnattendedUpgradeNotification notification, CancellationToken cancellationToken)
@@ -109,8 +137,13 @@ public class UnattendedUpgrader : INotificationAsyncHandler<RuntimeUnattendedUpg
try
{
await _packageMigrationRunner.RunPackagePlansAsync(pendingMigrations);
notification.UnattendedUpgradeResult = RuntimeUnattendedUpgradeNotification.UpgradeResult
.PackageMigrationComplete;
notification.UnattendedUpgradeResult = RuntimeUnattendedUpgradeNotification.UpgradeResult.PackageMigrationComplete;
// Migration plans may have changed published content, so refresh the distributed cache to ensure consistency on first request.
_distributedCache.RefreshAllPublishedSnapshot();
_logger.LogInformation(
"Migration plans run: {Plans}. Triggered refresh of distributed published content cache.",
string.Join(", ", pendingMigrations));
}
catch (Exception ex)
{
@@ -7,6 +7,7 @@ using Umbraco.Cms.Core.Cache;
using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Core.Migrations;
using Umbraco.Cms.Core.Models.Membership;
using Umbraco.Cms.Core.Models.PublishedContent;
using Umbraco.Cms.Core.PublishedCache;
using Umbraco.Cms.Core.Scoping;
using Umbraco.Cms.Core.Security;
@@ -51,9 +52,12 @@ public class MigrationPlanExecutor : IMigrationPlanExecutor
private readonly DistributedCache _distributedCache;
private readonly IScopeAccessor _scopeAccessor;
private readonly ICoreScopeProvider _scopeProvider;
private readonly IPublishedContentTypeFactory _publishedContentTypeFactory;
private bool _rebuildCache;
private bool _invalidateBackofficeUserAccess;
[Obsolete("Please use the constructor taking all parameters. Scheduled for removal in Umbraco 19.")]
public MigrationPlanExecutor(
ICoreScopeProvider scopeProvider,
IScopeAccessor scopeAccessor,
@@ -65,6 +69,33 @@ public class MigrationPlanExecutor : IMigrationPlanExecutor
IKeyValueService keyValueService,
IServiceScopeFactory serviceScopeFactory,
AppCaches appCaches)
: this(
scopeProvider,
scopeAccessor,
loggerFactory,
migrationBuilder,
databaseFactory,
databaseCacheRebuilder,
distributedCache,
keyValueService,
serviceScopeFactory,
appCaches,
StaticServiceProvider.Instance.GetRequiredService<IPublishedContentTypeFactory>())
{
}
public MigrationPlanExecutor(
ICoreScopeProvider scopeProvider,
IScopeAccessor scopeAccessor,
ILoggerFactory loggerFactory,
IMigrationBuilder migrationBuilder,
IUmbracoDatabaseFactory databaseFactory,
IDatabaseCacheRebuilder databaseCacheRebuilder,
DistributedCache distributedCache,
IKeyValueService keyValueService,
IServiceScopeFactory serviceScopeFactory,
AppCaches appCaches,
IPublishedContentTypeFactory publishedContentTypeFactory)
{
_scopeProvider = scopeProvider;
_scopeAccessor = scopeAccessor;
@@ -76,6 +107,7 @@ public class MigrationPlanExecutor : IMigrationPlanExecutor
_serviceScopeFactory = serviceScopeFactory;
_appCaches = appCaches;
_distributedCache = distributedCache;
_publishedContentTypeFactory = publishedContentTypeFactory;
_logger = _loggerFactory.CreateLogger<MigrationPlanExecutor>();
}
@@ -301,6 +333,7 @@ public class MigrationPlanExecutor : IMigrationPlanExecutor
_appCaches.IsolatedCaches.ClearAllCaches();
await _databaseCacheRebuilder.RebuildAsync(false);
_distributedCache.RefreshAllPublishedSnapshot();
_publishedContentTypeFactory.ClearDataTypeCache();
}
private async Task RevokeBackofficeTokens()
@@ -6,7 +6,9 @@ using Umbraco.Cms.Core.Configuration.Models;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Services.OperationStatus;
using Umbraco.Cms.Infrastructure.Persistence;
using Umbraco.Cms.Infrastructure.Persistence.Dtos;
using Umbraco.Extensions;
namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_16_3_0;
@@ -69,7 +71,7 @@ public class MigrateMediaTypeLabelProperties : AsyncMigrationBase
private void IfNotExistsCreateBytesLabel()
{
if (Database.Exists<NodeDto>(Constants.DataTypes.LabelBytes))
if (NodeExists(_labelBytesDataTypeKey))
{
return;
}
@@ -89,7 +91,7 @@ public class MigrateMediaTypeLabelProperties : AsyncMigrationBase
CreateDate = DateTime.Now,
};
_ = Database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, nodeDto);
Database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, nodeDto);
var dataTypeDto = new DataTypeDto
{
@@ -100,12 +102,12 @@ public class MigrateMediaTypeLabelProperties : AsyncMigrationBase
Configuration = "{\"umbracoDataValueType\":\"BIGINT\", \"labelTemplate\":\"{=value | bytes}\"}",
};
_ = Database.Insert(Constants.DatabaseSchema.Tables.DataType, "pk", false, dataTypeDto);
Database.Insert(Constants.DatabaseSchema.Tables.DataType, "pk", false, dataTypeDto);
}
private void IfNotExistsCreatePixelsLabel()
{
if (Database.Exists<NodeDto>(Constants.DataTypes.LabelPixels))
if (NodeExists(_labelPixelsDataTypeKey))
{
return;
}
@@ -125,7 +127,7 @@ public class MigrateMediaTypeLabelProperties : AsyncMigrationBase
CreateDate = DateTime.Now,
};
_ = Database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, nodeDto);
Database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, nodeDto);
var dataTypeDto = new DataTypeDto
{
@@ -136,7 +138,16 @@ public class MigrateMediaTypeLabelProperties : AsyncMigrationBase
Configuration = "{\"umbracoDataValueType\":\"INT\", \"labelTemplate\":\"{=value}px\"}",
};
_ = Database.Insert(Constants.DatabaseSchema.Tables.DataType, "pk", false, dataTypeDto);
Database.Insert(Constants.DatabaseSchema.Tables.DataType, "pk", false, dataTypeDto);
}
private bool NodeExists(Guid uniqueId)
{
Sql<ISqlContext> sql = Database.SqlContext.Sql()
.Select<NodeDto>(x => x.NodeId)
.From<NodeDto>()
.Where<NodeDto>(x => x.UniqueId == uniqueId);
return Database.FirstOrDefault<NodeDto>(sql) is not null;
}
private async Task MigrateMediaTypeLabels()
@@ -1381,9 +1381,13 @@ AND umbracoNode.id <> @id",
}
else if (ev.Key.langId.HasValue)
{
// This should never happen! If a property culture is flagged as edited then the culture must exist at the document level
throw new PanicException(
$"The existing DocumentCultureVariationDto was not found for node {ev.Key.nodeId} and language {ev.Key.langId}");
// This can happen when a property changes from invariant to variant and the content
// was only created in non-default languages. The invariant property data gets migrated
// to the default language, but no DocumentCultureVariationDto exists for the default
// language because the content was never created in that language.
// In this case, we simply skip updating the edited flag since there's no document
// culture variation record to update.
continue;
}
}
@@ -270,7 +270,7 @@ public class MediaRepository : ContentRepositoryBase<int, IMedia, MediaRepositor
"DELETE FROM " + Constants.DatabaseSchema.Tables.UserGroup2GranularPermission + " WHERE uniqueId IN (SELECT uniqueId FROM umbracoNode WHERE id = @id)",
"DELETE FROM " + Constants.DatabaseSchema.Tables.UserStartNode + " WHERE startNode = @id",
"UPDATE " + Constants.DatabaseSchema.Tables.UserGroup +
" SET startContentId = NULL WHERE startContentId = @id",
" SET startMediaId = NULL WHERE startMediaId = @id",
"DELETE FROM " + Constants.DatabaseSchema.Tables.Relation + " WHERE parentId = @id",
"DELETE FROM " + Constants.DatabaseSchema.Tables.Relation + " WHERE childId = @id",
"DELETE FROM " + Constants.DatabaseSchema.Tables.TagRelationship + " WHERE nodeId = @id",
@@ -51,7 +51,7 @@ public abstract class BlockEditorPropertyValueEditor<TValue, TLayout> : BlockVal
languageService,
ioHelper,
attribute,
StaticServiceProvider.Instance.GetRequiredService<ILogger>())
StaticServiceProvider.Instance.GetRequiredService<ILogger<BlockEditorPropertyValueEditor<TValue, TLayout>>>())
{
}
@@ -135,6 +135,8 @@ public abstract class BlockEditorPropertyValueEditor<TValue, TLayout> : BlockVal
BlockEditorData<TValue, TLayout>? currentBlockEditorData = SafeParseBlockEditorData(currentValue);
BlockEditorData<TValue, TLayout>? blockEditorData = SafeParseBlockEditorData(editorValue.Value);
CacheReferencedEntities(blockEditorData);
// We can skip MapBlockValueFromEditor if both editorValue and currentValue values are empty.
if (IsBlockEditorDataEmpty(currentBlockEditorData) && IsBlockEditorDataEmpty(blockEditorData))
{
@@ -43,6 +43,45 @@ public abstract class BlockValuePropertyValueEditorBase<TValue, TLayout> : DataV
_languageService = languageService;
}
/// <summary>
/// Caches referenced entities for all property values with supporting property editors within the specified block editor data
/// optimising subsequent retrieval of entities when parsing and converting property values.
/// </summary>
/// <remarks>
/// This method iterates through all property values associated with data editors in the provided
/// block editor data and invokes caching for referenced entities where supported by the property editor.
/// </remarks>
/// <param name="blockEditorData">The block editor data containing content and settings property values to analyze for referenced entities.</param>
[Obsolete("This method is available for support of request caching retrieved entities in derived property value editors. " +
"The intention is to supersede this with lazy loaded read locks, which will make this unnecessary. " +
"Scheduled for removal in Umbraco 19.")]
protected void CacheReferencedEntities(BlockEditorData<TValue, TLayout>? blockEditorData)
{
// Group property values by their associated data editor alias.
IEnumerable<IGrouping<string, BlockPropertyValue>> valuesByDataEditors = (blockEditorData?.BlockValue.ContentData ?? []).Union(blockEditorData?.BlockValue.SettingsData ?? [])
.SelectMany(x => x.Values)
.Where(x => x.EditorAlias is not null && x.Value is not null)
.GroupBy(x => x.EditorAlias!);
// Iterate through each group and cache referenced entities if supported by the data editor.
foreach (IGrouping<string, BlockPropertyValue> valueByDataEditor in valuesByDataEditors)
{
IDataEditor? dataEditor = _propertyEditors[valueByDataEditor.Key];
if (dataEditor is null)
{
continue;
}
IDataValueEditor valueEditor = dataEditor.GetValueEditor();
if (valueEditor is ICacheReferencedEntities valueEditorWithPrecaching)
{
valueEditorWithPrecaching.CacheReferencedEntities(valueByDataEditor.Select(x => x.Value!));
}
}
}
/// <inheritdoc />
public abstract IEnumerable<UmbracoEntityReference> GetReferences(object? value);
@@ -52,10 +52,8 @@ public class MediaPicker3PropertyEditor : DataEditor
/// <summary>
/// Defines the value editor for the media picker property editor.
/// </summary>
internal sealed class MediaPicker3PropertyValueEditor : DataValueEditor, IDataValueReference
internal sealed class MediaPicker3PropertyValueEditor : DataValueEditor, IDataValueReference, ICacheReferencedEntities
{
private const string MediaCacheKeyFormat = nameof(MediaPicker3PropertyValueEditor) + "_Media_{0}";
private readonly IDataTypeConfigurationCache _dataTypeReadCache;
private readonly IJsonSerializer _jsonSerializer;
private readonly IMediaImportService _mediaImportService;
@@ -107,6 +105,27 @@ public class MediaPicker3PropertyEditor : DataEditor
Validators.Add(validators);
}
/// <inheritdoc/>
public void CacheReferencedEntities(IEnumerable<object> values)
{
var mediaKeys = values
.SelectMany(value => Deserialize(_jsonSerializer, value))
.Select(dto => dto.MediaKey)
.Distinct()
.Where(x => IsMediaAlreadyCached(x, _appCaches.RequestCache) is false)
.ToList();
if (mediaKeys.Count == 0)
{
return;
}
IEnumerable<IMedia> mediaItems = _mediaService.GetByIds(mediaKeys);
foreach (IMedia media in mediaItems)
{
CacheMediaById(media, _appCaches.RequestCache);
}
}
/// <inheritdoc/>
public IEnumerable<UmbracoEntityReference> GetReferences(object? value)
{
@@ -208,31 +227,13 @@ public class MediaPicker3PropertyEditor : DataEditor
foreach (MediaWithCropsDto mediaWithCropsDto in mediaWithCropsDtos)
{
IMedia? media = GetMediaById(mediaWithCropsDto.MediaKey);
IMedia? media = GetAndCacheMediaById(mediaWithCropsDto.MediaKey, _appCaches.RequestCache, _mediaService);
mediaWithCropsDto.MediaTypeAlias = media?.ContentType.Alias ?? unknownMediaType;
}
return mediaWithCropsDtos.Where(m => m.MediaTypeAlias != unknownMediaType).ToList();
}
private IMedia? GetMediaById(Guid key)
{
// Cache media lookups in case the same media is handled multiple times across a save operation,
// which is possible, particularly if we have multiple languages and blocks.
var cacheKey = string.Format(MediaCacheKeyFormat, key);
IMedia? media = _appCaches.RequestCache.GetCacheItem<IMedia?>(cacheKey);
if (media is null)
{
media = _mediaService.GetById(key);
if (media is not null)
{
_appCaches.RequestCache.Set(cacheKey, media);
}
}
return media;
}
private List<MediaWithCropsDto> HandleTemporaryMediaUploads(List<MediaWithCropsDto> mediaWithCropsDtos, MediaPicker3Configuration configuration)
{
var invalidDtos = new List<MediaWithCropsDto>();
@@ -240,7 +241,7 @@ public class MediaPicker3PropertyEditor : DataEditor
foreach (MediaWithCropsDto mediaWithCropsDto in mediaWithCropsDtos)
{
// if the media already exist, don't bother with it
if (GetMediaById(mediaWithCropsDto.MediaKey) != null)
if (GetAndCacheMediaById(mediaWithCropsDto.MediaKey, _appCaches.RequestCache, _mediaService) != null)
{
continue;
}
@@ -480,18 +481,7 @@ public class MediaPicker3PropertyEditor : DataEditor
foreach (var typeAlias in distinctTypeAliases)
{
// Cache media type lookups since the same media type is likely to be used multiple times in validation,
// particularly if we have multiple languages and blocks.
var cacheKey = string.Format(MediaTypeCacheKeyFormat, typeAlias);
string? typeKey = _appCaches.RequestCache.GetCacheItem<string?>(cacheKey);
if (typeKey is null)
{
typeKey = _mediaTypeService.Get(typeAlias)?.Key.ToString();
if (typeKey is not null)
{
_appCaches.RequestCache.Set(cacheKey, typeKey);
}
}
string? typeKey = GetMediaTypeKey(typeAlias);
if (typeKey is null || allowedTypes.Contains(typeKey) is false)
{
@@ -506,6 +496,31 @@ public class MediaPicker3PropertyEditor : DataEditor
return [];
}
private string? GetMediaTypeKey(string typeAlias)
{
// Cache media type lookups since the same media type is likely to be used multiple times in validation,
// particularly if we have multiple languages and blocks.
string? GetMediaTypeKeyFromService(string typeAlias) => _mediaTypeService.Get(typeAlias)?.Key.ToString();
if (_appCaches.RequestCache.IsAvailable is false)
{
return GetMediaTypeKeyFromService(typeAlias);
}
var cacheKey = string.Format(MediaTypeCacheKeyFormat, typeAlias);
string? typeKey = _appCaches.RequestCache.GetCacheItem<string?>(cacheKey);
if (typeKey is null)
{
typeKey = GetMediaTypeKeyFromService(typeAlias);
if (typeKey is not null)
{
_appCaches.RequestCache.Set(cacheKey, typeKey);
}
}
return typeKey;
}
}
/// <summary>
@@ -3,7 +3,10 @@
using System.ComponentModel.DataAnnotations;
using System.Runtime.Serialization;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Umbraco.Cms.Core.Cache;
using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Core.IO;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.ContentEditing;
@@ -19,14 +22,16 @@ using Umbraco.Extensions;
namespace Umbraco.Cms.Core.PropertyEditors;
public class MultiUrlPickerValueEditor : DataValueEditor, IDataValueReference
public class MultiUrlPickerValueEditor : DataValueEditor, IDataValueReference, ICacheReferencedEntities
{
private readonly ILogger<MultiUrlPickerValueEditor> _logger;
private readonly IPublishedUrlProvider _publishedUrlProvider;
private readonly IJsonSerializer _jsonSerializer;
private readonly IContentService _contentService;
private readonly IMediaService _mediaService;
private readonly AppCaches _appCaches;
[Obsolete("Please use the constructor taking all parameters. Scheduled for removal in Umbraco 19.")]
public MultiUrlPickerValueEditor(
ILogger<MultiUrlPickerValueEditor> logger,
ILocalizedTextService localizedTextService,
@@ -37,19 +42,102 @@ public class MultiUrlPickerValueEditor : DataValueEditor, IDataValueReference
IIOHelper ioHelper,
IContentService contentService,
IMediaService mediaService)
: this(
logger,
localizedTextService,
shortStringHelper,
attribute,
publishedUrlProvider,
jsonSerializer,
ioHelper,
contentService,
mediaService,
StaticServiceProvider.Instance.GetRequiredService<AppCaches>())
{
}
public MultiUrlPickerValueEditor(
ILogger<MultiUrlPickerValueEditor> logger,
ILocalizedTextService localizedTextService,
IShortStringHelper shortStringHelper,
DataEditorAttribute attribute,
IPublishedUrlProvider publishedUrlProvider,
IJsonSerializer jsonSerializer,
IIOHelper ioHelper,
IContentService contentService,
IMediaService mediaService,
AppCaches appCaches)
: base(shortStringHelper, jsonSerializer, ioHelper, attribute)
{
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
_logger = logger;
_publishedUrlProvider = publishedUrlProvider;
_jsonSerializer = jsonSerializer;
_contentService = contentService;
_mediaService = mediaService;
_appCaches = appCaches;
Validators.Add(new TypedJsonValidatorRunner<LinkDisplay[], MultiUrlPickerConfiguration>(
_jsonSerializer,
new MinMaxValidator(localizedTextService)));
}
/// <inheritdoc/>
public void CacheReferencedEntities(IEnumerable<object> values)
{
var dtos = values
.Select(value =>
{
var asString = value is string str ? str : value.ToString();
if (string.IsNullOrEmpty(asString))
{
return null;
}
return _jsonSerializer.Deserialize<List<LinkDto>>(asString);
})
.WhereNotNull()
.SelectMany(x => x)
.Where(x => x.Type == Constants.UdiEntityType.Document || x.Type == Constants.UdiEntityType.Media)
.ToList();
IList<Guid> contentKeys = GetKeys(Constants.UdiEntityType.Document, dtos);
IList<Guid> mediaKeys = GetKeys(Constants.UdiEntityType.Media, dtos);
if (contentKeys.Count > 0)
{
IEnumerable<IContent> contentItems = _contentService.GetByIds(contentKeys);
foreach (IContent content in contentItems)
{
CacheContentById(content, _appCaches.RequestCache);
}
}
if (mediaKeys.Count > 0)
{
IEnumerable<IMedia> mediaItems = _mediaService.GetByIds(mediaKeys);
foreach (IMedia media in mediaItems)
{
CacheMediaById(media, _appCaches.RequestCache);
}
}
}
private IList<Guid> GetKeys(string entityType, IEnumerable<LinkDto> dtos) =>
dtos
.Where(x => x.Type == entityType)
.Select(x => x.Unique ?? (x.Udi is not null ? x.Udi.Guid : Guid.Empty))
.Where(x => x != Guid.Empty)
.Distinct()
.Where(x => IsAlreadyCached(x, entityType) is false)
.ToList();
private bool IsAlreadyCached(Guid key, string entityType) => entityType switch
{
Constants.UdiEntityType.Document => IsContentAlreadyCached(key, _appCaches.RequestCache),
Constants.UdiEntityType.Media => IsMediaAlreadyCached(key, _appCaches.RequestCache),
_ => false,
};
public IEnumerable<UmbracoEntityReference> GetReferences(object? value)
{
var asString = value == null ? string.Empty : value is string str ? str : value.ToString();
@@ -105,7 +193,7 @@ public class MultiUrlPickerValueEditor : DataValueEditor, IDataValueReference
if (dto.Udi.EntityType == Constants.UdiEntityType.Document)
{
url = _publishedUrlProvider.GetUrl(dto.Udi.Guid, UrlMode.Relative, culture);
IContent? c = _contentService.GetById(dto.Udi.Guid);
IContent? c = GetAndCacheContentById(dto.Udi.Guid, _appCaches.RequestCache, _contentService);
if (c is not null)
{
@@ -119,7 +207,7 @@ public class MultiUrlPickerValueEditor : DataValueEditor, IDataValueReference
else if (dto.Udi.EntityType == Constants.UdiEntityType.Media)
{
url = _publishedUrlProvider.GetMediaUrl(dto.Udi.Guid, UrlMode.Relative, culture);
IMedia? m = _mediaService.GetById(dto.Udi.Guid);
IMedia? m = GetAndCacheMediaById(dto.Udi.Guid, _appCaches.RequestCache, _mediaService);
if (m is not null)
{
published = m.Trashed is false;
@@ -207,6 +295,12 @@ public class MultiUrlPickerValueEditor : DataValueEditor, IDataValueReference
[DataMember(Name = "target")]
public string? Target { get; set; }
[DataMember(Name = "unique")]
public Guid? Unique { get; set; }
[DataMember(Name = "type")]
public string? Type { get; set; }
[DataMember(Name = "udi")]
public GuidUdi? Udi { get; set; }
@@ -1,6 +1,7 @@
// Copyright (c) Umbraco.
// See LICENSE for more details.
using System.Text.Json;
using Microsoft.Extensions.Logging;
using Umbraco.Cms.Core.Models.DeliveryApi;
using Umbraco.Cms.Core.Models.PublishedContent;
@@ -53,10 +54,10 @@ public class ImageCropperValueConverter : PropertyValueConverterBase, IDeliveryA
{
value = _jsonSerializer.Deserialize<ImageCropperValue>(sourceString);
}
catch (Exception ex)
catch (JsonException ex)
{
// cannot deserialize, assume it may be a raw image URL
_logger.LogError(ex, "Could not deserialize string '{JsonString}' into an image cropper value.", sourceString);
// Cannot deserialize, assume it may be a raw image URL.
_logger.LogDebug(ex, "Could not deserialize string '{JsonString}' into an image cropper value.", sourceString);
value = new ImageCropperValue { Src = sourceString };
}
@@ -108,7 +108,7 @@ internal sealed class DatabaseCacheRebuilder : IDatabaseCacheRebuilder
_logger.LogWarning(
"Database cache was serialized using {CurrentSerializer}. Currently configured cache serializer {Serializer}. Rebuilding database cache.",
currentSerializer,
currentSerializer == 0 ? "None" : currentSerializer,
serializer);
using (_profilingLogger.TraceDuration<DatabaseCacheRebuilder>($"Rebuilding database cache with {serializer} serializer"))
@@ -1,4 +1,3 @@

using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using Umbraco.Cms.Core.Configuration.Models;
@@ -74,7 +73,7 @@ public static class UmbracoBuilderExtensions
builder.AddNotificationAsyncHandler<ContentTypeDeletedNotification, CacheRefreshingNotificationHandler>();
builder.AddNotificationAsyncHandler<MediaTypeRefreshedNotification, CacheRefreshingNotificationHandler>();
builder.AddNotificationAsyncHandler<MediaTypeDeletedNotification, CacheRefreshingNotificationHandler>();
builder.AddNotificationAsyncHandler<UmbracoApplicationStartedNotification, SeedingNotificationHandler>();
builder.AddNotificationAsyncHandler<UmbracoApplicationStartingNotification, SeedingNotificationHandler>();
builder.AddCacheSeeding();
return builder;
}
@@ -1,3 +1,4 @@
using System.Collections.Concurrent;
using Microsoft.Extensions.Caching.Hybrid;
namespace Umbraco.Cms.Infrastructure.HybridCache.Extensions;
@@ -7,19 +8,24 @@ namespace Umbraco.Cms.Infrastructure.HybridCache.Extensions;
/// </summary>
internal static class HybridCacheExtensions
{
// Per-key semaphores to ensure the GetOrCreateAsync + RemoveAsync sequence
// executes atomically for a given cache key.
private static readonly ConcurrentDictionary<string, SemaphoreSlim> _keyLocks = new();
/// <summary>
/// Returns true if the cache contains an item with a matching key.
/// </summary>
/// <param name="cache">An instance of <see cref="Microsoft.Extensions.Caching.Hybrid.HybridCache"/></param>
/// <param name="key">The name (key) of the item to search for in the cache.</param>
/// <param name="token">The cancellation token.</param>
/// <returns>True if the item exists already. False if it doesn't.</returns>
/// <remarks>
/// Hat-tip: https://github.com/dotnet/aspnetcore/discussions/57191
/// Will never add or alter the state of any items in the cache.
/// </remarks>
public static async Task<bool> ExistsAsync<T>(this Microsoft.Extensions.Caching.Hybrid.HybridCache cache, string key)
public static async Task<bool> ExistsAsync<T>(this Microsoft.Extensions.Caching.Hybrid.HybridCache cache, string key, CancellationToken token)
{
(bool exists, _) = await TryGetValueAsync<T>(cache, key);
(bool exists, _) = await TryGetValueAsync<T>(cache, key, token).ConfigureAwait(false);
return exists;
}
@@ -29,34 +35,55 @@ internal static class HybridCacheExtensions
/// <typeparam name="T">The type of the value of the item in the cache.</typeparam>
/// <param name="cache">An instance of <see cref="Microsoft.Extensions.Caching.Hybrid.HybridCache"/></param>
/// <param name="key">The name (key) of the item to search for in the cache.</param>
/// <param name="token">The cancellation token.</param>
/// <returns>A tuple of <see cref="bool"/> and the object (if found) retrieved from the cache.</returns>
/// <remarks>
/// Hat-tip: https://github.com/dotnet/aspnetcore/discussions/57191
/// Will never add or alter the state of any items in the cache.
/// </remarks>
public static async Task<(bool Exists, T? Value)> TryGetValueAsync<T>(this Microsoft.Extensions.Caching.Hybrid.HybridCache cache, string key)
public static async Task<(bool Exists, T? Value)> TryGetValueAsync<T>(this Microsoft.Extensions.Caching.Hybrid.HybridCache cache, string key, CancellationToken token)
{
var exists = true;
T? result = await cache.GetOrCreateAsync<object, T>(
key,
null!,
(_, _) =>
{
exists = false;
return new ValueTask<T>(default(T)!);
},
new HybridCacheEntryOptions(),
null,
CancellationToken.None);
// Acquire a per-key semaphore so that GetOrCreateAsync and the possible RemoveAsync
// complete without another thread retrieving/creating the same key in-between.
SemaphoreSlim sem = _keyLocks.GetOrAdd(key, _ => new SemaphoreSlim(1, 1));
// In checking for the existence of the item, if not found, we will have created a cache entry with a null value.
// So remove it again.
if (exists is false)
await sem.WaitAsync().ConfigureAwait(false);
try
{
await cache.RemoveAsync(key);
}
T? result = await cache.GetOrCreateAsync<T?>(
key,
cancellationToken =>
{
exists = false;
return default;
},
new HybridCacheEntryOptions(),
null,
token).ConfigureAwait(false);
return (exists, result);
// In checking for the existence of the item, if not found, we will have created a cache entry with a null value.
// So remove it again. Because we're holding the per-key lock there is no chance another thread
// will observe the temporary entry between GetOrCreateAsync and RemoveAsync.
if (exists is false)
{
await cache.RemoveAsync(key).ConfigureAwait(false);
}
return (exists, result);
}
finally
{
sem.Release();
// Only remove the semaphore mapping if it still points to the same instance we used.
// This avoids removing another thread's semaphore or corrupting the map.
if (_keyLocks.TryGetValue(key, out SemaphoreSlim? current) && ReferenceEquals(current, sem))
{
_keyLocks.TryRemove(key, out _);
}
}
}
}
@@ -38,7 +38,7 @@ internal sealed class PublishedContentFactory : IPublishedContentFactory
/// <inheritdoc/>
public IPublishedContent? ToIPublishedContent(ContentCacheNode contentCacheNode, bool preview)
{
var cacheKey = $"{nameof(PublishedContentFactory)}DocumentCache_{contentCacheNode.Id}_{preview}";
var cacheKey = $"{nameof(PublishedContentFactory)}DocumentCache_{contentCacheNode.Id}_{preview}_{contentCacheNode.Data?.VersionDate.Ticks ?? 0}";
IPublishedContent? publishedContent = null;
if (_appCaches.RequestCache.IsAvailable)
{
@@ -1,4 +1,4 @@
using Microsoft.Extensions.Options;
using Microsoft.Extensions.Options;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Configuration.Models;
using Umbraco.Cms.Core.Events;
@@ -9,7 +9,7 @@ using Umbraco.Cms.Infrastructure.HybridCache.Services;
namespace Umbraco.Cms.Infrastructure.HybridCache.NotificationHandlers;
internal sealed class SeedingNotificationHandler : INotificationAsyncHandler<UmbracoApplicationStartedNotification>
internal sealed class SeedingNotificationHandler : INotificationAsyncHandler<UmbracoApplicationStartingNotification>
{
private readonly IDocumentCacheService _documentCacheService;
private readonly IMediaCacheService _mediaCacheService;
@@ -24,7 +24,7 @@ internal sealed class SeedingNotificationHandler : INotificationAsyncHandler<Umb
_globalSettings = globalSettings.Value;
}
public async Task HandleAsync(UmbracoApplicationStartedNotification notification,
public async Task HandleAsync(UmbracoApplicationStartingNotification notification,
CancellationToken cancellationToken)
{
@@ -205,7 +205,7 @@ internal sealed class DocumentCacheService : IDocumentCacheService
var cacheKey = GetCacheKey(key, false);
var existsInCache = await _hybridCache.ExistsAsync<ContentCacheNode>(cacheKey);
var existsInCache = await _hybridCache.ExistsAsync<ContentCacheNode?>(cacheKey, cancellationToken).ConfigureAwait(false);
if (existsInCache is false)
{
uncachedKeys.Add(key);
@@ -278,7 +278,7 @@ internal sealed class DocumentCacheService : IDocumentCacheService
return false;
}
return await _hybridCache.ExistsAsync<ContentCacheNode>(GetCacheKey(keyAttempt.Result, preview));
return await _hybridCache.ExistsAsync<ContentCacheNode?>(GetCacheKey(keyAttempt.Result, preview), CancellationToken.None);
}
public async Task RefreshContentAsync(IContent content)
@@ -133,7 +133,7 @@ internal sealed class MediaCacheService : IMediaCacheService
return false;
}
return await _hybridCache.ExistsAsync<ContentCacheNode>($"{keyAttempt.Result}");
return await _hybridCache.ExistsAsync<ContentCacheNode?>($"{keyAttempt.Result}", CancellationToken.None);
}
public async Task RefreshMediaAsync(IMedia media)
@@ -170,7 +170,7 @@ internal sealed class MediaCacheService : IMediaCacheService
var cacheKey = GetCacheKey(key, false);
var existsInCache = await _hybridCache.ExistsAsync<ContentCacheNode>(cacheKey);
var existsInCache = await _hybridCache.ExistsAsync<ContentCacheNode?>(cacheKey, CancellationToken.None);
if (existsInCache is false)
{
uncachedKeys.Add(key);
@@ -1,5 +1,8 @@
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Options;
using Umbraco.Cms.Core.Configuration.Models;
using Umbraco.Cms.Core.Persistence.Repositories;
using Umbraco.Cms.Core.Web;
using Umbraco.Extensions;
namespace Umbraco.Cms.Web.Common.Repositories;
@@ -11,21 +14,35 @@ internal sealed class WebProfilerRepository : IWebProfilerRepository
private const string QueryName = "umbDebug";
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly ICookieManager _cookieManager;
private readonly GlobalSettings _globalSettings;
public WebProfilerRepository(IHttpContextAccessor httpContextAccessor)
public WebProfilerRepository(IHttpContextAccessor httpContextAccessor, ICookieManager cookieManager, IOptions<GlobalSettings> globalSettings)
{
_httpContextAccessor = httpContextAccessor;
_cookieManager = cookieManager;
_globalSettings = globalSettings.Value;
}
public void SetStatus(int userId, bool status)
{
if (status)
{
_httpContextAccessor.GetRequiredHttpContext().Response.Cookies.Append(CookieName, "1", new CookieOptions { Expires = DateTime.Now.AddYears(1) });
// This cookie enables debug profiling on the front-end without needing query strings or headers.
// It uses SameSite=Strict, so it only works when the BackOffice and front-end share the same domain.
// It's marked httpOnly to prevent JavaScript access (the server reads it, not client-side code).
// No expiration is set, so it's a session cookie and will be deleted when the browser closes.
// For cross-site setups, use the query string (?umbDebug=true) or header (X-UMB-DEBUG) instead.
_cookieManager.SetCookieValue(
CookieName,
"1",
httpOnly: true,
secure: _globalSettings.UseHttps,
sameSiteMode: "Strict");
}
else
{
_httpContextAccessor.GetRequiredHttpContext().Response.Cookies.Delete(CookieName);
_cookieManager.ExpireCookie(CookieName);
}
}
@@ -43,6 +60,6 @@ internal sealed class WebProfilerRepository : IWebProfilerRepository
return xUmbDebug;
}
return request.Cookies.ContainsKey(CookieName);
return _cookieManager.HasCookie(CookieName);
}
}
+11 -1
View File
@@ -26,6 +26,7 @@ If you have an existing Vite server running, you can run the task **Backoffice A
### Run a Front-end server against a local Umbraco instance
#### 1. Configure Umbraco instance
Enable the front-end server communicating with the Backend server(Umbraco instance) you need need to correct the `appsettings.json` of your project.
For code contributions use the backend project of `/src/Umbraco.Web.UI`.
@@ -38,7 +39,11 @@ Open this file in an editor: `/src/Umbraco.Web.UI/appsettings.Development.json`
"BackOfficeHost": "http://localhost:5173",
"AuthorizeCallbackPathName": "/oauth_complete",
"AuthorizeCallbackLogoutPathName": "/logout",
"AuthorizeCallbackErrorPathName": "/error",
"AuthorizeCallbackErrorPathName": "/error",,
"BackOfficeTokenCookie": {
"Enabled": true,
"SameSite": "None"
}
},
},
}
@@ -46,10 +51,15 @@ Open this file in an editor: `/src/Umbraco.Web.UI/appsettings.Development.json`
This will override the backoffice host URL, enabling the Client to run from a different origin.
> [!NOTE]
> If you get stuck in a login loop, try clearing your browser cookies for localhost, and make sure that the `BackOfficeTokenCookie` settings are correct. Namely, that `SameSite` should be set to `None` when running the front-end server separately.
#### 2. Start Umbraco
Then start the backend server by running the command: `dotnet run` in the `/src/Umbraco.Web.UI` folder.
#### 3. Start Frontend server
Now start the frontend server by running the command: `npm run dev:server` in the `/src/Umbraco.Web.UI.Client` folder.
Finally open `http://localhost:5173` in your browser.
+8
View File
@@ -2,6 +2,14 @@
This package contains the types for the Umbraco Backoffice.
## Preview
A live preview of the latest backoffice build from the main branch is available at:
**[backofficepreview.umbraco.com](https://backofficepreview.umbraco.com/)**
This preview is automatically deployed via GitHub Actions whenever changes are pushed to main or version branches.
## Installation
```bash
File diff suppressed because it is too large Load Diff
+4 -4
View File
@@ -1,7 +1,7 @@
{
"name": "@umbraco-cms/backoffice",
"license": "MIT",
"version": "16.4.0-rc",
"version": "16.6.0-rc",
"type": "module",
"exports": {
".": null,
@@ -212,8 +212,8 @@
"generate:ui-api-docs": "npm run generate:check-const-test && typedoc --options typedoc.config.js"
},
"engines": {
"node": ">=22",
"npm": ">=10.9"
"node": ">=22.17.1",
"npm": ">=10.9.2"
},
"dependencies": {
"element-internals-polyfill": "^3.0.2"
@@ -261,7 +261,7 @@
"typescript": "5.9.3",
"typescript-eslint": "^8.45.0",
"typescript-json-schema": "^0.65.1",
"vite": "^7.1.9",
"vite": "^7.1.11",
"vite-plugin-static-copy": "^3.1.3",
"vite-tsconfig-paths": "^5.1.4",
"web-component-analyzer": "^2.0.0"
@@ -83,8 +83,9 @@ export class UmbBackofficeHeaderSectionsElement extends UmbLitElement {
const clickedSectionAlias = manifest.alias;
// If the clicked section is the same as the current section, we just load the original section path to load the section root
if (this._currentSectionAlias === clickedSectionAlias) {
// If preventUrlRetention is set to true then go to the section root.
// Or if the clicked section is the current active one, then navigate to the section root
if (manifest?.meta.preventUrlRetention === true || this._currentSectionAlias === clickedSectionAlias) {
const sectionPath = this.#getSectionPath(manifest);
history.pushState(null, '', sectionPath);
return;
@@ -6,7 +6,7 @@ import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
export class UmbPreviewExitElement extends UmbLitElement {
async #onClick() {
const previewContext = await this.getContext(UMB_PREVIEW_CONTEXT);
previewContext?.exitPreview(0);
await previewContext?.exitPreview(0);
}
override render() {
@@ -6,7 +6,7 @@ import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
export class UmbPreviewOpenWebsiteElement extends UmbLitElement {
async #onClick() {
const previewContext = await this.getContext(UMB_PREVIEW_CONTEXT);
previewContext?.openWebsite();
await previewContext?.openWebsite();
}
override render() {
@@ -1,10 +1,12 @@
import { UmbBooleanState, UmbStringState } from '@umbraco-cms/backoffice/observable-api';
import { tryExecute } from '@umbraco-cms/backoffice/resources';
import { umbConfirmModal } from '@umbraco-cms/backoffice/modal';
import { DocumentService } from '@umbraco-cms/backoffice/external/backend-api';
import { UmbBooleanState, UmbStringState } from '@umbraco-cms/backoffice/observable-api';
import { UmbContextBase } from '@umbraco-cms/backoffice/class-api';
import { UmbContextToken } from '@umbraco-cms/backoffice/context-api';
import { UmbDocumentPreviewRepository } from '@umbraco-cms/backoffice/document';
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
import { UMB_SERVER_CONTEXT } from '@umbraco-cms/backoffice/server';
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
const UMB_LOCALSTORAGE_SESSION_KEY = 'umb:previewSessions';
@@ -89,6 +91,19 @@ export class UmbPreviewContext extends UmbContextBase {
});
}
async #getPublishedUrl(): Promise<string | null> {
if (!this.#unique) return null;
// NOTE: We should be reusing `UmbDocumentUrlRepository` here, but the preview app doesn't register the `itemStore` extensions, so can't resolve/consume `UMB_DOCUMENT_URL_STORE_CONTEXT`. [LK]
const { data } = await tryExecute(this, DocumentService.getDocumentUrls({ query: { id: [this.#unique] } }));
if (!data?.length) return null;
const urlInfo = this.#culture ? data[0].urlInfos.find((x) => x.culture === this.#culture) : data[0].urlInfos[0];
if (!urlInfo?.url) return null;
return urlInfo.url.startsWith('/') ? `${this.#serverUrl}${urlInfo.url}` : urlInfo.url;
}
#getSessionCount(): number {
return Math.max(Number(localStorage.getItem(UMB_LOCALSTORAGE_SESSION_KEY)), 0) || 0;
}
@@ -170,7 +185,12 @@ export class UmbPreviewContext extends UmbContextBase {
this.#webSocket = undefined;
}
const url = this.#previewUrl.getValue() as string;
let url = await this.#getPublishedUrl();
if (!url) {
url = this.#previewUrl.getValue() as string;
}
window.location.replace(url);
}
@@ -190,8 +210,13 @@ export class UmbPreviewContext extends UmbContextBase {
return this.getHostElement().shadowRoot?.querySelector('#wrapper') as HTMLElement;
}
openWebsite() {
const url = this.#previewUrl.getValue() as string;
async openWebsite() {
let url = await this.#getPublishedUrl();
if (!url) {
url = this.#previewUrl.getValue() as string;
}
window.open(url, '_blank');
}
@@ -63,7 +63,7 @@ export class UmbPreviewElement extends UmbLitElement {
src=${this._previewUrl}
title="Page preview"
@load=${this.#onIFrameLoad}
sandbox="allow-scripts allow-same-origin"></iframe>
sandbox="allow-scripts allow-same-origin allow-forms"></iframe>
</div>
</div>
<div id="menu">
@@ -2525,13 +2525,19 @@ export default {
profiling: {
performanceProfiling: 'Performance profiling',
performanceProfilingDescription:
"<p>Umbraco currently runs in debug mode. This means you can use the built-in performance profiler to assess the performance when rendering pages.</p><p>If you want to activate the profiler for a specific page rendering, simply add <strong>umbDebug=true</strong> to the querystring when requesting the page.</p><p>If you want the profiler to be activated by default for all page renderings, you can use the toggle below. It will set a cookie in your browser, which then activates the profiler automatically. In other words, the profiler will only be active by default in <em>your</em> browser - not everyone else's.</p>",
"<p>Umbraco currently runs in debug mode. This means you can use the built-in performance profiler to assess the performance when rendering pages.</p><p>If you want to activate the profiler for a specific page rendering, simply add <strong>umbDebug=true</strong> to the querystring when requesting the page.</p><p>If you want the profiler to be activated by default for all page renderings, you can use the toggle below. It will set a cookie in your browser, which then activates the profiler automatically. In other words, the profiler will only be active by default in <em>your</em> browser - not everyone else's.</p><p><strong>Note:</strong> This will only work if the Backoffice is currently located on the same URL as the front-end website.</p>",
activateByDefault: 'Activate the profiler by default',
reminder: 'Friendly reminder',
reminderDescription:
'<p>You should never let a production site run in debug mode. Debug mode is turned off by setting <strong>Umbraco:CMS:Hosting:Debug</strong> to <strong>false</strong> in appsettings.json, appsettings.{Environment}.json or via an environment variable.</p>',
profilerEnabledDescription:
"<p>Umbraco currently does not run in debug mode, so you can't use the built-in profiler. This is how it should be for a production site.</p><p>Debug mode is turned on by setting <strong>Umbraco:CMS:Hosting:Debug</strong> to <strong>true</strong> in appsettings.json, appsettings.{Environment}.json or via an environment variable.</p>",
errorEnablingProfilerTitle: 'Error enabling profiler',
errorEnablingProfilerDescription:
'It was not possible to enable the profiler. Check that you are accessing the Backoffice on the same URL as the front-end website, and try again. If the problem persists, please check the log for more details.',
errorDisablingProfilerTitle: 'Error disabling profiler',
errorDisablingProfilerDescription:
'It was not possible to disable the profiler. Try again, and if the problem persists, please check the log for more details.',
},
settingsDashboardVideos: {
trainingHeadline: 'Hours of Umbraco training videos are only a click away',
@@ -2869,29 +2875,52 @@ export default {
ar: 'العربية',
bs: 'Bosanski',
cs: 'Česky',
'cs-cz': 'Česky (Czechia)',
cy: 'Cymraeg',
'cy-gb': 'Cymraeg (UK)',
da: 'Dansk',
'da-dk': 'Dansk (Danmark)',
de: 'Deutsch',
'de-de': 'Deutsch (Deutschland)',
'de-ch': 'Deutsch (Schweiz)',
en: 'English (UK)',
'en-us': 'English (US)',
es: 'Español',
'es-es': 'Español (España)',
fr: 'Français',
he: 'Hebrew',
'fr-fr': 'Français (France)',
'fr-ch': 'Français (Suisse)',
he: 'עברית',
'he-il': 'עברית (ישראל)',
hr: 'Hrvatski',
'hr-hr': 'Hrvatski (Hrvatska)',
it: 'Italiano',
'it-it': 'Italiano (Italia)',
'it-ch': 'Italiano (Svizzera)',
ja: '日本語',
'ja-jp': '日本語 (日本)',
ko: '한국어',
'ko-kr': '한국어 (한국)',
nb: 'Norsk Bokmål',
'nb-no': 'Norsk (Bokmål)',
nl: 'Nederlands',
'nl-nl': 'Nederlands (Nederland)',
pl: 'Polski',
'pl-pl': 'Polski (Polska)',
pt: 'Português',
'pt-br': 'Português (Brasil)',
ro: 'Romana',
ro: 'Română',
'ro-ro': 'Română (România)',
ru: 'Русский',
'ru-ru': 'Русский (Россия)',
sv: 'Svenska',
'sv-se': 'Svenska (Sverige)',
tr: 'Türkçe',
'tr-tr': 'Türkçe (Türkiye Cumhuriyeti)',
uk: 'Українська',
'uk-ua': 'Українська (Україна)',
zh: '中文',
'zh-cn': '中文(简体,中国)',
'zh-tw': '中文(正體,台灣)',
vi: 'Tiếng Việt',
},
@@ -2836,34 +2836,4 @@ export default {
resetUrlMessage: 'Bạn có chắc chắn muốn đặt lại URL này không?',
resetUrlLabel: 'Đặt lại',
},
uiCulture: {
ar: 'العربية',
bs: 'Bosanski',
cs: 'Česky',
cy: 'Cymraeg',
da: 'Dansk',
de: 'Deutsch',
en: 'English (UK)',
'en-us': 'English (US)',
es: 'Español',
fr: 'Français',
he: 'Hebrew',
hr: 'Hrvatski',
it: 'Italiano',
ja: '日本語',
ko: '한국어',
nb: 'Norsk Bokmål',
nl: 'Nederlands',
pl: 'Polski',
pt: 'Português',
'pt-br': 'Português (Brasil)',
ro: 'Romana',
ru: 'Русский',
sv: 'Svenska',
tr: 'Türkçe',
uk: 'Українська',
zh: '中文',
'zh-tw': '中文(正體,台灣)',
vi: 'Tiếng Việt',
},
} as UmbLocalizationDictionary;
@@ -77,6 +77,41 @@ export class RedirectRequestHandler extends AuthorizationRequestHandler {
});
}
/**
* Cleanup all stale authorization requests and configurations from storage.
* This scans localStorage for any keys matching the appauth patterns and removes them,
* including the authorization request handle key.
*/
public cleanupStaleAuthorizationData(): Promise<void> {
// Check if we're in a browser environment with localStorage
if (typeof window === 'undefined' || !window.localStorage) {
return Promise.resolve();
}
const keysToRemove: string[] = [];
// Scan localStorage for all appauth-related keys
for (let i = 0; i < window.localStorage.length; i++) {
const key = window.localStorage.key(i);
if (
key &&
(key.includes('_appauth_authorization_request') ||
key.includes('_appauth_authorization_service_configuration') ||
key === AUTHORIZATION_REQUEST_HANDLE_KEY)
) {
keysToRemove.push(key);
}
}
// Remove all found stale keys
const removePromises = keysToRemove.map((key) => this.storageBackend.removeItem(key));
return Promise.all(removePromises).then(() => {
if (keysToRemove.length > 0) {
log(`Cleaned up ${keysToRemove.length} stale authorization data entries`);
}
});
}
/**
* Attempts to introspect the contents of storage backend and completes the
* request.
@@ -119,12 +154,8 @@ export class RedirectRequestHandler extends AuthorizationRequestHandler {
} else {
authorizationResponse = new AuthorizationResponse({ code: code, state: state });
}
// cleanup state
return Promise.all([
this.storageBackend.removeItem(AUTHORIZATION_REQUEST_HANDLE_KEY),
this.storageBackend.removeItem(authorizationRequestKey(handle)),
this.storageBackend.removeItem(authorizationServiceConfigurationKey(handle)),
]).then(() => {
// cleanup all authorization data including current and stale entries
return this.cleanupStaleAuthorizationData().then(() => {
log('Delivering authorization response');
return {
request: request,
@@ -134,7 +165,10 @@ export class RedirectRequestHandler extends AuthorizationRequestHandler {
});
} else {
log('Mismatched request (state and request_uri) dont match.');
return Promise.resolve(null);
// cleanup all authorization data even on mismatch to prevent stale PKCE data
return this.cleanupStaleAuthorizationData().then(() => {
return null;
});
}
})
);
@@ -35,6 +35,7 @@ export class FetchRequestor extends Requestor {
const requestInit: RequestInit = {};
requestInit.method = settings.method;
requestInit.mode = 'cors';
requestInit.credentials = settings.credentials ?? 'include';
if (settings.data) {
if (settings.method && settings.method.toUpperCase() === 'POST') {
+2 -2
View File
@@ -6,7 +6,7 @@
"build": "vite build"
},
"dependencies": {
"@umbraco-ui/uui": "^1.16.0-rc.0",
"@umbraco-ui/uui-css": "^1.16.0-rc.0"
"@umbraco-ui/uui": "^1.16.0",
"@umbraco-ui/uui-css": "^1.16.0"
}
}

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