Compare commits

...
191 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 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 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 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
Kenn JacobsenandAndy Butland 369b020d9d Explicitly flush isolated caches by key for content updates (#20519)
* Explicitly flush isolated caches by key for content updates

* Apply suggestions from code review

---------

Co-authored-by: Andy Butland <abutland73@gmail.com>
2025-10-16 14:17:04 +02:00
Laura Neto ec354cef92 Merge branch 'release/16.3'
# Conflicts:
#	src/Umbraco.Web.UI.Client/package-lock.json
#	src/Umbraco.Web.UI.Client/package.json
#	version.json
2025-10-16 10:05:33 +02:00
Warren BuckleyandGitHub 4c05a114c5 Fixes 20476 - Changes icon to be no entry sign (#20496) 2025-10-15 14:10:39 +02:00
Andy ButlandandGitHub e71f36d816 Back Office: Fixes link to workspace root from breadcrumb trail (closes: #20455) (#20459)
Fixes link to workspace root from breadcrumb trail.
2025-10-15 10:06:00 +02:00
Andy ButlandandGitHub fdf759d08d Content Types: Prevent creation of document type with an alias that case insensitively matches an existing alias (closes #20467) (#20471)
Prevent creation of document type with an alias that case insensitively matches an existing alias.
2025-10-15 09:41:41 +02:00
1ab13a970b Dashboard: Add tests for create and using custom dashboard (#20253)
* add tests for custom dashboard

* update test dashboard using helper

* remove extensionRegistry for playwright config

* update helper version for dashboard

* Update tests/Umbraco.Tests.AcceptanceTest/tests/ExtensionRegistry/CustomDashboard.spec.ts

Co-authored-by: Nhu Dinh <150406148+nhudinh0309@users.noreply.github.com>

* fix format code

---------

Co-authored-by: Lan Nguyen Thuy <lnt@umbraco.dk>
Co-authored-by: Nhu Dinh <150406148+nhudinh0309@users.noreply.github.com>
2025-10-15 13:33:41 +07:00
e22b459d9c WorkspaceView: Add tests for create and using custom workspace view (#20408)
* WorkspaceView: Add tests for create and using custom workspace view

* update helper version

* Update tests/Umbraco.Tests.AcceptanceTest/tests/ExtensionRegistry/WorkspaceView.spec.ts

Co-authored-by: Nhu Dinh <150406148+nhudinh0309@users.noreply.github.com>

* Update tests/Umbraco.Tests.AcceptanceTest/tests/ExtensionRegistry/WorkspaceView.spec.ts

Co-authored-by: Nhu Dinh <150406148+nhudinh0309@users.noreply.github.com>

* update format code

* Update tests/Umbraco.Tests.AcceptanceTest/tests/ExtensionRegistry/WorkspaceView.spec.ts

Co-authored-by: Nhu Dinh <150406148+nhudinh0309@users.noreply.github.com>

* Update tests/Umbraco.Tests.AcceptanceTest/tests/ExtensionRegistry/WorkspaceView.spec.ts

Co-authored-by: Nhu Dinh <150406148+nhudinh0309@users.noreply.github.com>

* Update tests/Umbraco.Tests.AcceptanceTest/tests/ExtensionRegistry/WorkspaceView.spec.ts

Co-authored-by: Nhu Dinh <150406148+nhudinh0309@users.noreply.github.com>

---------

Co-authored-by: Lan Nguyen Thuy <lnt@umbraco.dk>
Co-authored-by: Nhu Dinh <150406148+nhudinh0309@users.noreply.github.com>
2025-10-15 13:33:24 +07:00
CopilotGitHubcopilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>iOvergaardleekelleher
a19b9fb5fe UFM: Add camelCase aliases for UFM filters to support UFMJS expressions (closes #20500) (#20501)
* Initial plan

* Add camelCase aliases for UFM filters with hyphens (stripHtml, titleCase, wordLimit)

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

* Add manifest tests for camelCase filter aliases

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

* discards tests that are not useful

* test: updates imports for stripHtml api

* Exports `UmbUfmStripHtmlFilterApi` class

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: iOvergaard <752371+iOvergaard@users.noreply.github.com>
Co-authored-by: leekelleher <leekelleher@gmail.com>
2025-10-14 16:35:42 +00:00
Anders ReusandGitHub cdf9ee4566 Added culture to the ApiContentRouteBuilder to include variant languages. (#20366) (#20499)
Added culture to the ApiContentRouteBuilder to include variant languages.
2025-10-14 16:06:48 +02:00
Sven GeusensandGitHub e53220c8f5 Delivery API: Fix not reindexing branch descendants when branch root already published but unchanged (closes #20370) (#20462)
* Fix deliveryApi not reindexing branch descendants when branch root already published and unchanged

* Commit update and name improvement
2025-10-14 12:15:01 +02:00
Andy ButlandandGitHub 12adfd52bd Performance: Reduce number of database calls in save and publish operations (#20485)
* Added request caching to media picker media retrieval, to improve performance in save operations.

* WIP: Update or insert in bulk when updating property data.

* Add tests verifying UpdateBatch.

* Fixed issue with UpdateBatch and SQL Server.

* Removed stopwatch.

* Fix test on SQLite (failing on SQLServer).

* Added temporary test for direct call to NPoco UpdateBatch.

* Fixed test on SQLServer.

* Add integration test verifying the same property data is persisted as before the performance refactor.

* Log expected warning in DocumentUrlService as debug.
2025-10-14 11:22:21 +02:00
494674d354 Entity Actions: More create button discernible text, extension of #20434 (#20458)
* added hovering and focus border to RTE

* fix main to OG

* fix to main again

* I'm going to cry

* Missing localiztion feature, maybe UmbLitElement?

* added localization controller to fetch localized version

* localization successful for viewActionsFor and CreateFor

* clean up button text

* Changed label for content header to display proper name

* clean up code

* Included button labels for media section

* clean code

* Relocated localization keys,

as `actions_viewActionsFor` already existed.

Also made into a function, to support a fallback label.

* Simplified the "Create for" label/localization

Removed the need for a `getCreateAriaLabel()` method.

* Removed the double-localizations (of `actions_viewActionsFor`)

as the "umb-entity-actions-bundle" component handles this now.

* imports tidy-up

* Simplified localization key condition

* switched to new localization key for other sections for new labeling

* Bumped `@umbraco/playwright-testhelpers` 16.0.55

https://github.com/umbraco/Umbraco.Playwright.Testhelpers/releases/tag/release%2F16.0.55

---------

Co-authored-by: Oskar kruger <obk@umbraco.dk>
Co-authored-by: leekelleher <leekelleher@gmail.com>
2025-10-14 07:20:01 +00:00
Lee KelleherandGitHub 4ba186633c UFM: Adds $index support to Block editors (fixes #20470) (#20488)
* Block List: adds `$index` support for UFM labels

* Block Grid: adds `$index` support for UFM labels

* Block RTE: adds `$index` support for UFM labels

Which is always zero `0`.
But has been wired up if we do implement the index order in future.
2025-10-14 08:46:48 +02:00
Andy Butland e336f9dfb0 Merge branch 'release/16.3'
# Conflicts:
#	version.json
2025-10-14 08:22:07 +02:00
Lee KelleherandGitHub 3ac37f3686 Recycle Bin: Trigger cache invalidation for trashed document/media items (#20483)
* Configure document/media items to listen for `Trashed` server-events for cache invalidation

* Fire reload event on restore destination tree/menu

* Removed "trashed" part of the code comment
2025-10-13 16:32:48 +01:00
0a027dd80d Dependencies: Fixed dependency conflicts when installing Microsoft.EntityFrameworkCore.Design (closes #20421) (#20474)
* Add explicit references to Microsoft.CodeAnalysis.* packages to fix conflicts when installing Microsoft.EntityFrameworkCore.Design

This allows consumers to simply install Microsoft.EntityFrameworkCore.Design without having to manually install specific versions to deal with transitive dependency problems.

* Disable CPM for UI project to better reflect consumers

* Update src/Umbraco.Infrastructure/Umbraco.Infrastructure.csproj

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

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-10-13 13:12:45 +02:00
Bjarne FyrstenborgandGitHub 7b4684cd70 UX: Center align log type in media history view (#20469)
Center align log type in media
2025-10-13 06:34:02 +02:00
3df8b9e41a Refactoring: Fixed spelling mistake in method name (#20460)
* Fixed spelling mistake in method name.

* Apply suggestion from @Copilot

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

---------

Co-authored-by: Kenn Jacobsen <kja@umbraco.dk>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-10-10 13:00:47 +00:00
Bjarne FyrstenborgandGitHub fd34ce5bd7 Icon Picker: Fit icons scroll container to modal height (#20438)
* Fill height and align icons to top

* Auto scrollbar instead

* Auto height of grid rows

* Enforce scroll again
2025-10-10 10:32:19 +02:00
99c2aaf17a Members: Forward port of fix for member lockout issue #16988 from PR #17007 for 16 (#20441)
* Port PR #17007

* Update src/Umbraco.Infrastructure/Security/IdentityMapDefinition.cs

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

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-10-10 09:52:57 +02:00
a4c373d3b5 Entity Actions: Create button discernible text (fixes #20205) (#20434)
* added hovering and focus border to RTE

* fix main to OG

* fix to main again

* I'm going to cry

* Missing localiztion feature, maybe UmbLitElement?

* added localization controller to fetch localized version

* localization successful for viewActionsFor and CreateFor

* clean up button text

* Changed label for content header to display proper name

* clean up code

* Included button labels for media section

* clean code

* Relocated localization keys,

as `actions_viewActionsFor` already existed.

Also made into a function, to support a fallback label.

* Simplified the "Create for" label/localization

Removed the need for a `getCreateAriaLabel()` method.

* Removed the double-localizations (of `actions_viewActionsFor`)

as the "umb-entity-actions-bundle" component handles this now.

* imports tidy-up

* Simplified localization key condition

---------

Co-authored-by: Oskar kruger <obk@umbraco.dk>
Co-authored-by: leekelleher <leekelleher@gmail.com>
2025-10-09 13:56:30 +00:00
bcedc8de2a Emails: Add Expires header (#20285)
* Add `Expiry` header to emails, set default expiry to 30 days and allow user config via `appsettings`

* Remove `IsSmtpExpirationConfigured` as it will always have a value

* Check for `emailExpiration` value

* Removed `EmailExpiration` default value as it should be opt-in

* Simplify SMTP email expiration condition

* Fix APICompat issue

* Add implementation to `NotImplementedEmailSender`

* Rename `emailExpiration` to `expires` to match the SMTP header

* Obsolete interfaces without `expires` parameter, delegate to an existing method.

* Set expiry TimeSpan values from user configurable settings with defaults

* Fix formating

* Handle breaking changes, add obsoletion messages and simplify interfaces.

* Fix default of invite expires timespan (was being parsed as 72 days not 72 hours).

---------

Co-authored-by: Andy Butland <abutland73@gmail.com>
2025-10-09 14:27:53 +02:00
767894b723 Color Picker: Validate uniqueness of selected colors (#20431)
* Added unique color checker to color picker.

* Added Unittest for duplicates

* optimized for codescene

* removed the bump and simplified the function

* Fixed behaviour for duplicate checks so unit test passes.
A little refactoring.

* Adds continue so invalid colors aren't checked for duplicates.

---------

Co-authored-by: Andy Butland <abutland73@gmail.com>
2025-10-09 11:50:11 +02:00
1fe7931d07 Migrations: Adjust the JsonBlockValueConverter to handle conflicts with 'values' property (#20429)
* Adjust the `JsonBlockValueConverter` to handle conflicts with 'values' property (due to old data schema)

* Simplify code

* Add unit test to verify change.

---------

Co-authored-by: Andy Butland <abutland73@gmail.com>
2025-10-09 09:41:41 +02:00
Andy ButlandandGitHub 16132b0075 Update Umbraco version for LTS release in template 2025-10-09 06:37:21 +02:00
Niels LyngsøandGitHub 9a18dd547d Content Type Designer: Fix moving a group to an inherited tab (#20138)
move group to inherited tab
2025-10-08 16:49:48 +00:00
Niels LyngsøandGitHub 7ecc6ece7e Block Workspace: dynamic headline & browser title (#20424)
* get content type name method

* use local virtual render for the workspace headline and browser title
2025-10-08 15:40:42 +00:00
a676dc4bad Announce newest notification using live region (#19979)
* Added screen readers notification support

* Making the sr-live div not visible for users

* Moved aria-live outside the repeat

---------

Co-authored-by: Niels Lyngsø <nsl@umbraco.dk>
2025-10-08 15:27:56 +00:00
9fe8b2162b Adjust styling for inherited content type property (#20261)
* Adjust styling for inherited content type property

* Slight style adjustments inherited property

---------

Co-authored-by: leekelleher <leekelleher@gmail.com>
2025-10-08 15:22:09 +00:00
Mads RasmussenandGitHub 429f8b0bed Permissions: Added User fallback permission condition (closes #20097) (#20224)
* add user fallback permission condition

* add example
2025-10-08 16:17:35 +01:00
Niels LyngsøandGitHub 1a462bcc7d Content Workspace: Invariant Breadcrumbs does not work (fix #20423) (#20428)
fix navigating to an invariant document via breadcrumb
2025-10-08 15:09:15 +00:00
eaac515b30 V16/bug/20035 validation context uncaught promise (#20036)
* #20035 Updated validation context example to reflect issue

* Fixes #20035 by handling returned promise

* just catch if it was rejected

---------

Co-authored-by: Niels Lyngsø <niels.lyngso@gmail.com>
2025-10-08 12:33:04 +00:00
c6e5df40c9 Backoffice: Add Entity Signs (overlay icons) to tree items. (#20328)
* entity signs folder

* update package.json

* entity sign extension type

* implement entity sign extension

* POC document has collection sign

* implement icon kind

* rename file

* note about this being wrong

* move type

* change import

* entity sign bundle element

* implement icon kind label

* Display icon and show popover on hover

* Fix the popover logic

* Moving the sign icon to the iconContainer to handle position

* fix missing document tree icon

* revert removal of icon slot render

* remove unused styles

* document tree item - inherit styles from the base element

* correctly extend styles

* revert document tree item icon change

* move icon container html

* add method to get an icon name

* Adding delay to the popover when opens

* Add animation to popover when it opens

* Making the parent of the entity bundle trigger popover on hover

* Display 2 icons over the main icon

* Updating some styles

* Position one icon on top of the other and add css style variables

* Changing popover-container for position-anchor

* generate server types

* Using css properties to display and animate the signs

* Stacked icons using grid property

* Use translate property to move the icons around

* Added fallback styles for firefox

* formatting of state properties

* implement entity flags across content types

* lint fixes

* fix import extension mess

* await both properties for this to work

* transfer flags to entity sign bundle ext initializer

* is-protected entity sign

* Made signs infobox show downward.

* Changed px to rems

* Change the manifest for the actual signs we will display

* add icon color, remove unused label, add weight

* changes styles + animation + slotted icon inside

* Overwrite pending changes when schedule is active and added green color to schedule.

* adjust animation

* add background for sign

* avoid re-rendering when properties are being set

* Bind the flags to each sign manifest.

* increase signs offset

* fix document tree item draft style

* Removed unused exports.

* Remove duplicated hover timer logic.

* Added eslint disable line to keep the empty method for future implementation.

* rename class

* Rename interface for optional entity flags

* make alias more explicit to prevent future collisions

* include alias in field name to make it clear that we do not except all colors

* align function names with conventions

* always include flags in document items

* compose tree types

* set up entity-flag module and move related types

* change label

---------

Co-authored-by: Niels Lyngsø <niels.lyngso@gmail.com>
Co-authored-by: Mads Rasmussen <madsr@hey.com>
2025-10-08 10:40:27 +00:00
Engiber LozadaandGitHub 193d8af9af Icon Picker: Deselect current icon & add “No icon” tile inside the modal. (#20342)
* Show selected icon and color(if any) when open the modal.

* Add a button inside the modal that clears the value

* Deselect the value if we click the already selected icon.

* Add placeholder icon and some localization for labels

* Remove unused variable

* Added config for the placeholder icon in case no icon is selected.
2025-10-08 10:28:44 +00:00
dc92cf4b19 Add accessibility label for splitview divider (#20380)
* added hovering and focus border to RTE

* fix main to OG

* fix to main again

* I'm going to cry

* Added label for splitviewdivider

* Added localization to divider label and updated common lang files

* Removes unused import

---------

Co-authored-by: Oskar kruger <obk@umbraco.dk>
Co-authored-by: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com>
2025-10-08 11:11:40 +01:00
Jacob Overgaard edf95e6fab adds deprecation notices to AllowNonExistingSegmentsCreation 2025-10-08 09:58:22 +02:00
Sebastiaan Janssen adf8708987 Move yml file to the correct location 2025-10-08 09:17:43 +02:00
Sebastiaan JanssenandGitHub de2b15d89e Add workflow to create release discussions from labels 2025-10-08 09:15:37 +02:00
Andy ButlandandGitHub 913b79d682 Performance: Added request cache to media type retrieval in media picker validation (#20405)
* Added request cache to media type retrieval in media picker validation.

* Applied suggestions from code review.
2025-10-08 07:56:49 +02:00
Jacob OvergaardandGitHub 4197ca637b build(deps): bumps @umbraco-ui/uui from 1.15.0 to 1.16.0-rc.0 (#20402) 2025-10-08 04:29:03 +00:00
Laura NetoandGitHub df81264794 Integration tests: Fix failing SQLServer integration tests (#20406)
Fix failing SQLServer integration tests

Adjusted the tests so that the created content is retrieved again after creation, instead of using the returned IContent.
This is needed because SQLServer, when using datetime, rounds to the closest .000, .003, or .007, which would cause the comparisons to fail.
We should consider moving away from datetime to datetime2, as the former should be avoided according to Microsoft.

https://learn.microsoft.com/en-us/sql/t-sql/data-types/datetime-transact-sql?view=sql-server-ver17
2025-10-07 16:55:31 +02:00
Piers TaylorandGitHub e9ae3cf5db Repositories: Retrieve users for groups in batches to fix UserService.GetAllInGroup failing on too many users in a group (#20298)
* fix: Identified everywhere the bugs happen and implemented the InGroupOf() extension to successfully batch the SQL queries

* Added helper function in case this batching functionality is in future scopes

* Accidently deleted a groupIds.Any() check while adding BatchFetch helper function

* Removed helper function and instead utilising built in FetchByGroups extension method
2025-10-07 16:09:24 +02:00
Jacob Overgaard f5da0db977 chore: generate new openapi types 2025-10-07 14:43:31 +02:00
CopilotGitHubcopilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>iOvergaard
cb8c331777 UFM: Fixes the truncate filter to only add ellipsis when text is actually truncated (closes #20395) (#20396)
* Initial plan

* Fix UFM truncate filter to only add ellipsis when text is actually truncated

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

* feat: trims string before evaluating

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: iOvergaard <752371+iOvergaard@users.noreply.github.com>
2025-10-07 14:19:11 +02:00
Nhu DinhandGitHub 4af9b1e156 User Profile: QA Add acceptance tests for the regression issue #18084 (#20329)
* Added tests for changing own password

* Updated tests for current user profile

* Bumped version

* Make CurrentUserProfile tests run in the pipeline

* Added step to ensure the error notification toast not displays

* Reverted npm command
2025-10-07 09:17:58 +00:00
Jacob OvergaardandGitHub b4e97ea49f Dependencies: Updates all npm dependencies for Client, Login, and UmbracoExtension (#20386)
* build(deps): bump backoffice dependencies

* revert types

* build(deps): updates @hey-api/openapi-ts to 0.85.0 and regenerates files on client and template

* build(deps): updates all icons + regenerate

* build(deps): bumps all login dependencies + regenerate api

* chore: runs eslint --fix

* chore: runs prettier fix on files

* chore: fixes imports
2025-10-07 07:14:23 +00:00
Nhu DinhandGitHub d400ba5899 Delivery API: QA Add acceptance test for media delivery api (#20182)
* Added tests for media delivery api

* Added tests for content delivery api

* Fixed import

* Updated skip tag and issue link for the failing tests

* Bumped version

* Split delivery api tests into 2 files

* Updated tests for media delivery Api

* Cleaned up

* Fixed comments

* Fixed comments
2025-10-07 06:58:16 +00:00
Andy ButlandandGitHub 184c17e2c8 Hybrid cache: Check for ContentCacheNode instead of object on exists for hybrid cache to ensure correct deserialization (closes #20352) (#20383)
Checked for ContentCacheNode instead of object on exists for hybrid cache to ensure correct deserialization.
2025-10-06 21:20:16 +02:00
Bjarne FyrstenborgandGitHub 4330a99830 Block Grid: Styling of group drag with handle (#20361)
* Adjustment of block grid group drag with handle

* Adjust input, handle and delete button
2025-10-06 18:12:43 +02:00
Bjarne FyrstenborgandGitHub fa5f6b9acc Block Grid: Configure minimum one column for areas (#20363)
Configure minimum 1 column for areas in block grid
2025-10-06 18:10:34 +02:00
7819b6dfac Value Presets: also run value presets on load (#20239)
* term example

* better localization options

* localize range

* ensure range value handling

* extract lox high from value setting

* further improvements

* stop requiring entity-type for values

* setup for parsing blueprints as values to the value preset manager

* write test for blueprint values in value preset controller

* deprecate scaffold method in order to use a new more generic name

* Avoid manipulating the incoming data

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

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

* use max here

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-10-06 15:15:38 +02:00
Jacob Overgaard 11b62518ae Removed v10/dev, added v17/dev 2025-10-06 13:39:55 +02:00
Nhu DinhandGitHub f3d121d91f Content: QA Add acceptance tests for regression issue #17735 (#20330)
* Added tests for duplicate a content

* Bumped version

* Make all tests for duplicating a content run in the pipeline

* Fixed comments

* Reverted npm command
2025-10-06 10:54:18 +00:00
84fb1f4d6c Extension Registry: QA Added acceptance tests for readOnlyGuards rule, entity action and custom property editor (#19939)
* Added tests for readOnlyGuard rules

* Added backoffice override files for readOnlyGuard tests

* Bumped version

* Added project for ExtensionRegistry tests in playwright configs

* Updated nightly E2E test pipelines to run Extension Registry tests

* Updated nightly E2E test pipeline

* Updated nightly E2E test pipeline

* Updated playwright configs

* Updated nightly E2E test pipeline

* Add test for Entity Action Extension to retrieve entityType and unique (#20020)

* Add entity action test to get unique and entity type

* update test entity action

---------

Co-authored-by: Lan Nguyen Thuy <lnt@umbraco.dk>

* Added job to run the Extension Registry tests in the nightly pipeline

* Cleaned up

* Restructure AdditionSetup folder for extension registry

* Updated yaml file for nightly E2E pipeline

* Updated json file for lock action

* Skip test for content delivery API

* Updated port

* Comment out others to run only extension registry tests

* Updated port

* Remove retrieve action folder to test

* Reverted nightly E2E test pipeline

* Reverted

* Updated umbraco package json

* Reverted

* Renamed AdditionalSetup folder

* Renamed folder

* Added appsetting.json file

* Updated appsettings.json

* Updated appsettings.json

* Added debug step

* Added step to build backoffice

* Reverted

* Only spec.ts file run in the extension registry project

* Property Editor: Add tests for create and using custom property editor (#20213)

* Property Editor: tests for create and using custom property editor

* Update tests/Umbraco.Tests.AcceptanceTest/tests/ExtensionRegistry/PropertyEditorTest.spec.ts

Co-authored-by: Nhu Dinh <150406148+nhudinh0309@users.noreply.github.com>

* Update tests/Umbraco.Tests.AcceptanceTest/tests/ExtensionRegistry/PropertyEditorTest.spec.ts

Co-authored-by: Nhu Dinh <150406148+nhudinh0309@users.noreply.github.com>

* Update tests/Umbraco.Tests.AcceptanceTest/tests/ExtensionRegistry/PropertyEditorTest.spec.ts

Co-authored-by: Nhu Dinh <150406148+nhudinh0309@users.noreply.github.com>

* update review from Nhu

* Update tests/Umbraco.Tests.AcceptanceTest/tests/ExtensionRegistry/CustomPropertyEditor.spec.ts

Co-authored-by: Nhu Dinh <150406148+nhudinh0309@users.noreply.github.com>

* Update tests/Umbraco.Tests.AcceptanceTest/tests/ExtensionRegistry/CustomPropertyEditor.spec.ts

Co-authored-by: Nhu Dinh <150406148+nhudinh0309@users.noreply.github.com>

* fix comment from Nhu

---------

Co-authored-by: Lan Nguyen Thuy <lnt@umbraco.dk>
Co-authored-by: Nhu Dinh <150406148+nhudinh0309@users.noreply.github.com>

* Format code

* Fixed

* Format code

* Format code

* Format code

* Updated indentation

* Fixed comments

* change the name of test

---------

Co-authored-by: NguyenThuyLan <116753400+NguyenThuyLan@users.noreply.github.com>
Co-authored-by: Lan Nguyen Thuy <lnt@umbraco.dk>
Co-authored-by: Andreas Zerbst <andr317c@live.dk>
2025-10-06 09:47:06 +00:00
Niels LyngsøandGitHub e7fde8c01f Entity-Action Bundle: Ensure First Action are updated in rendering (#20351)
ensure requesting rendering update
2025-10-06 11:43:10 +02:00
Niels LyngsøandGitHub a95a2b955f Loader bar: Implement loader for Content Workspaces (#20288)
* no need for a ctrl alias here

* added ctrl aliases and clean up unused css

* implement view loader
2025-10-06 11:41:50 +02:00
02b93e90cb Slider: improved value fallback handling + validation (#20228)
* term example

* better localization options

* localize range

* ensure range value handling

* extract lox high from value setting

* further improvements

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

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

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-10-06 11:28:38 +02:00
16c0de803b Block Grid: Add a bit more spacing and align button in block grid areas config (#20374)
* Add a bit more spacing and align button in block grid areas config

* Remove unnecessary blank line in CSS

---------

Co-authored-by: Andy Butland <abutland73@gmail.com>
2025-10-06 05:20:25 +00:00
Bjarne FyrstenborgandGitHub e80cc1a9f0 Block Grid: Add more spacing in combobox group header (#20373)
Add a bit more spacing in combobox group header
2025-10-06 06:37:00 +02:00
963331448a APIs: Use EndpointMetadata to check for MapToApiAttribute at runtime to include Controller in Swagger document (#20144)
* Use EndpointMetadata to check for existing MapToApiAttribute at runtime

* fix api breaking change

* revert MethodInfoApiCommonExtensions.cs

* remove empty line in ActionDescriptorApiCommonExtensions.cs

* Add xml comments to ActionDescriptorApiCommonExtensions

* Revert boy scout refactoring to primary constructur

* Better xml comments in ActionDescriptorApiCommonExtensions

---------

Co-authored-by: Marcus Wilhelmson <marcus.wilhelmson@consid.se>
2025-10-03 16:42:39 +02:00
Bjarne FyrstenborgandGitHub e5c942b459 Block Grid: Add icon to allowed block types in area (#20364)
* Add icon to allowed block types in area

* Adjust type with icon property

* Use umb-icon instead

* Add styling for combo list option
2025-10-03 11:23:47 +02:00
28fa373b0c Change to layout of default Content area dashboard (#20355)
Change to layout of default content dashboard.

I have removed the max width on the wrapper and increased the padding on the small pods to match the larger pod. This improves consistency with all other default dashboards in other sections of the CMS, none of which had a max width applied and where all pods had larger padding.

Co-authored-by: Paul <paul@madebycrunch.com>
2025-10-03 11:21:52 +02:00
Andreas ZerbstandGitHub e0449d3c00 E2E Pipeline: QA Remove continue on error condition (#20365)
Cherrypicked changes
2025-10-03 09:02:10 +02:00
Bjarne FyrstenborgandGitHub 5a565b7e8d Localization: Added translations for area validation property labels (#20362)
Localization of property labels
2025-10-03 08:03:16 +02:00
Bjarne FyrstenborgandGitHub cd1cbb3b15 Color Picker: Styling for drag handle at swatch items (#20360)
Add styling for drag handle at color picker item input
2025-10-03 07:53:21 +02:00
Bjarne FyrstenborgandGitHub 36dbab2289 Color Picker: Set initial hex value for picker (#20357)
* Set intial color for hidden color input

* Expand 3-digits hex to 6-digits
2025-10-03 07:47:36 +02:00
Carl SargunarandMatthew-Wise 3f8ad2238f trust certificate (for local dev only) 2025-10-02 20:07:00 +01:00
Carl SargunarandMatthew-Wise 42c7b744c5 update tool path 2025-10-02 20:07:00 +01:00
CarlandMatthew-Wise 589285d081 Switch container to use SQL 2022 instead of SQL Edge 2025-10-02 20:07:00 +01:00
61277b3812 Caching: Fixes regression of the caching of null representations for missing dictionary items (closes #20336 for 16) (#20349)
* Ports fix to regression of the caching of null representations for missing dictionary items.

* Fixed error raised in code review.

---------

Co-authored-by: Kenn Jacobsen <kja@umbraco.dk>
2025-10-02 18:10:16 +00:00
e6d1d7a6b9 Data Type: Improve missing property editor and property editor UI feedback (#20339)
* Adjust data type workspace UI when opening a data type that has an editor or editor UI that could not be found

* Also display a custom UI in a document property where the editor UI could not be found

* Fix circular dependency

* Small renames

* provide data-path for property editor picker

* update console warning

* Text copy changes

* add comment to element

* Fix editor alias not updating when selecting a different property editor UI

* Remove outdated comment

---------

Co-authored-by: Niels Lyngsø <niels.lyngso@gmail.com>
Co-authored-by: Niels Lyngsø <nsl@umbraco.dk>
2025-10-02 17:41:11 +02:00
Niels LyngsøandGitHub 5d64f51003 Segments: Auto unfold the culture variant in non-culture-variant mode (#20189)
in segment only variant situation then auto unfold the culture variant
2025-10-02 16:57:21 +02:00
Mathias HelsengrenandGitHub 1f747d3b7f Login form: Improved accessibility by removing duplicate label and aria-label (#20232)
Removed Aria label from the username and password input, since we already have <label> tag
2025-10-02 16:49:22 +02:00
Niels LyngsøandGitHub b35db9cb5b Handle vary by segment variant-entires (#20191)
* refactor code

* display language name for empty names

* ensure all culture variants when entering a segment-shared value, shared across cultures

* Revert parts of "ensure all culture variants when entering a segment-shared value, shared across cultures"

This reverts commit 0e64f72695.
2025-10-02 14:51:34 +02:00
436be6ec3f Exception handling: Improve error messaging on invalid umbraco-package.json file (#20332)
* Improve error messaging on invalid umbraco-package.json file.

* Adjust failing unit tests

---------

Co-authored-by: Laura Neto <12862535+lauraneto@users.noreply.github.com>
2025-10-02 13:03:00 +02:00
Laura Neto cf61356b80 Merge branch 'release/16.3' 2025-10-02 12:22:29 +02:00
AlexandGitHub 0d98527c58 Media: Prevent svg image to extend outside the editor bounds (#20346)
Prevent svg image to extend outside the editor bounds
2025-10-02 07:02:56 +02:00
78f4caa2be Refactoring: Add extension method for retrieval of language ISO codes if that's all we need (#20324)
* Retrieve only ISO codes from the database rather than full language objects if that's all we need.

* Apply suggestions from code review

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

* Removed repository updates and migrated the new service method to an extension method.

* Fixed issue after merge.

* Removed left-over using

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Kenn Jacobsen <kja@umbraco.dk>
2025-10-01 18:21:41 +00:00
Anders ReusandGitHub 3f0428c8ef Removed the dot "." in front of the jpg file extension example. (#20334) 2025-10-01 15:46:33 +02:00
Erik-Jan WestendorpandGitHub b261628b4b Extend logging (add delay and period) (#20333) 2025-10-01 11:54:27 +00:00
a84d67eff8 Migrations: Create missing tabs on content types when referenced by both composition and content type groups (closes #20058) (#20303)
* Add migration to create missing tabs

In v13, if a tab had groups in both a composition and the content type, the tab might not exist on the content type itself.
Newer versions require such tabs to also exist directly on the content type. This migration ensures those tabs are created.

Also fixes an issue in LeftJoin where nested sql arguments were being discarded.

* Small fixes

* WIP: Integration test.

* Added asserts to show the current issue with the integration test.

* Adjusted the integration test

* Added logging of result. Minor re-order and extraction refactoring in integration test.

---------

Co-authored-by: Andy Butland <abutland73@gmail.com>
2025-10-01 09:39:56 +02:00
Bjarne FyrstenborgandGitHub 1082bf17b2 Fix localized message in block group delete confirm modal (#20312)
* Fix localized message in block group delete confirm modal

* Add translation for block configuration removal

* Fix typo

* Fix a few translations

* Adjust translation
2025-09-30 07:01:28 +00:00
418043bf59 Log Viewer: Fixes continuous polling (closes #20274) (#20310)
* LogViewer: Fixes continuous polling (closes #20274)

* Update src/Umbraco.Web.UI.Client/src/packages/log-viewer/workspace/logviewer-workspace.context.ts

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

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-09-30 07:31:00 +02:00
65393ff3d9 Models: PropertyType constructor sets the DataTypeKey if IDataType has identity (#20301)
* PropertyType constructor sets the DataTypeKey if passed IDataType has identity

* Updated unit tests to verify behaviour.

---------

Co-authored-by: Andy Butland <abutland73@gmail.com>
2025-09-30 05:18:26 +00:00
Niels LyngsøandGitHub 82fc41a459 Media Workspace Path: add parent path to support absolute path generation (#20302)
add parent path to support absolute path generation
2025-09-29 19:21:55 +01:00
Nhu DinhandGitHub 1ca7f3162f E2E nightly pipeline: QA Fix failing acceptance tests for invalid cases (#20289)
* Updated steps to verify the error validation message

* Updated default extension for Tiptap

* Removed skip tag for fixed smoke tests

* Bumped version
2025-09-29 15:12:38 +02:00
Jacob Overgaard a30f837384 Merge remote-tracking branch 'origin/release/16.3' 2025-09-29 13:17:08 +02:00
Jacob Overgaard 2758813547 Merge branch 'release/16.3' 2025-09-29 13:16:43 +02:00
HenrikandGitHub fbbbc45c24 Micro-optimization: Move compilation of some Regex generation to compile time instead of runtime, make some static ones compiled (#20287)
Move compilation of some Regex generation to compile time instead of runtime, make some static ones compiled
2025-09-28 23:40:50 +02:00
Janus HasseriisandMatthew-Wise fc60b5b5ff Add custom property for #headerColumn 2025-09-26 09:25:28 +01:00
Mads RasmussenandGitHub 84811046f1 AllowEditInvariantFromNonDefault : Fix property write guard to use correct variant ID (closes #20250) (#20277)
Fix property write guard to use correct variant ID

Replaces the use of propertyVariantId with _datasetVariantId in the property write guard check to ensure permissions are evaluated for the correct variant.
2025-09-26 09:25:51 +02:00
7572ef5fff NuGet vulnerability warnings: Warn in non-Release mode, Error in non-Release mode (#17244)
* Initial adjustment of the projects with package vulnerabilities that errored, to change to ignore the four specific Nuget vulnerability warnings in Debug mode (but not Release) as per https://learn.microsoft.com/en-us/nuget/reference/errors-and-warnings/nu1901-nu1904 (NU1901,NU1902,NU1903,NU1904)

* Fixed formatting errors with tests

* No trailing whitespace

* Move NuGet vulnerability warnings error suppression to Directory.Build.props, combine WarningsNotAsErrors and fix minor issues

* Update Umbraco.JsonSchema.csproj

Removed unwanted change

* Update Umbraco.JsonSchema.csproj

Removed unwanted change

* Revert unecessary changes since merge

* Tweak more unecessary changes

* Small tweaks

* Remove space

* Reverted spacing changes

* Remove no longer required warning exclusions

* Reverted unwanted change

* Reversed order

* A few tweaks to reduce warnings in Umbraco.TestData

* More warnings removed as no longer an issue

---------

Co-authored-by: Ronald Barendse <ronald@barend.se>
Co-authored-by: Emma Garland <emma.garland@rocksolidknowledge.com>
Co-authored-by: Jason Elkin <jasonelkin86@gmail.com>
2025-09-25 15:31:36 +02:00
61f1c4abd0 Fixes issue with service registration for file system tree service (#20273)
* Display the latest update date in document collection view

* Fix issue with service registration of physical file system tree service.

* Apply suggestions from code review

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

---------

Co-authored-by: Laura Neto <12862535+lauraneto@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-09-25 13:13:02 +00:00
dependabot[bot]andJacob Overgaard cf10a59a4c Bump tar-fs from 3.1.0 to 3.1.1 in /src/Umbraco.Web.UI.Client
Bumps [tar-fs](https://github.com/mafintosh/tar-fs) from 3.1.0 to 3.1.1.
- [Commits](https://github.com/mafintosh/tar-fs/compare/v3.1.0...v3.1.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-09-25 14:09:27 +02:00
Bjarne FyrstenborgandGitHub 149b191cf4 Add slightly more padding at top so user card outline is not cutoff (#20258) 2025-09-25 13:54:01 +02:00
Nhu DinhandGitHub 9e3b27cfdd QA Skip the known failing smoke test to avoid blocking other PRs (#20269)
Added skip for the failing smoke test
2025-09-25 09:31:05 +00:00
Owain WilliamsandGitHub 54b49f90bd Update Readme to signpost the Forum (#20268)
Update README.md with information about the forum

Making a small change to the Readme to signpost the Forum now that it's the place to go for help/questions
2025-09-25 10:33:53 +02:00
Bjarne FyrstenborgandGitHub e93802769c Make remove button consistent with other multi value item remove button (#20257) 2025-09-24 21:19:14 +02:00
Laura Neto 1aa8bdf3aa Bump version to 16.4.0-rc 2025-09-24 14:01:34 +02:00
Andreas ZerbstandGitHub a8cdba6e70 Tiptap: QA Added acceptance tests for regression issue #19763 (#20226)
* Added test

* Finished up test

* Updated tests

* Removed comment

* Updated testCommand

* Reverted smokeTest
2025-09-24 11:05:00 +00:00
Niels LyngsøandGitHub 6001d94f50 Add more icons correct Content Type Designer Icons (#20252)
* add more icons correct Content Type Designer Icons

* add icon
2025-09-24 10:55:00 +00:00
557 changed files with 13036 additions and 4154 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.
```
+10 -1
View File
@@ -38,9 +38,17 @@ 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 both a social space but also has channels for questions and answers. Feel free to lurk or join in with your own questions. Or just post your daily Wordle score, up to you!
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.
## Looking to contribute back to Umbraco?
@@ -52,3 +60,4 @@ You came to the right place! Our GitHub repository is available for all kinds of
Umbraco is contribution-focused and community-driven. If you want to contribute back to the Umbraco source code, please check out our [guide to contributing](CONTRIBUTING.md).
### Tip: You should not run Umbraco from source code found here. Umbraco is extremely extensible and can do whatever you need. Instead, [install Umbraco as noted above](#looking-to-install-umbraco) and then [extend it any way you want to](https://docs.umbraco.com/umbraco-cms/extending/).
+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:
@@ -0,0 +1,163 @@
name: Create a release discussions for each new version label
on:
schedule:
- cron: "0 * * * *" # every hour
workflow_dispatch: # allow manual runs
permissions:
contents: read
discussions: write
issues: read
pull-requests: read
jobs:
reconcile:
runs-on: ubuntu-latest
steps:
- name: Reconcile release/* labels → discussions
uses: actions/github-script@v7
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const owner = context.repo.owner;
const repo = context.repo.repo;
const categoryName = "Releases";
// 24h cutoff
const since = new Date(Date.now() - 24*60*60*1000).toISOString();
core.info(`Scanning issues/PRs updated since ${since}`);
// fetch repo + discussion categories
const repoData = await github.graphql(`
query($owner:String!, $repo:String!){
repository(owner:$owner, name:$repo){
id
discussionCategories(first:100){ nodes { id name } }
}
}
`, { owner, repo });
const repoId = repoData.repository.id;
const category = repoData.repository.discussionCategories.nodes.find(c => c.name === categoryName);
if (!category) {
core.setFailed(`Discussion category "${categoryName}" not found`);
return;
}
const categoryId = category.id;
// paginate issues/PRs updated in last 24h
for await (const { data: items } of github.paginate.iterator(
github.rest.issues.listForRepo,
{ owner, repo, state: "all", since, per_page: 100 }
)) {
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/") && n !== "release/no-notes");
if (releaseLabels.length === 0) continue;
core.info(`#${item.number}: ${releaseLabels.join(", ")}`);
for (const labelName of releaseLabels) {
const version = labelName.substring("release/".length);
const titleTarget = `Release: ${version}`;
// search discussions
let discussionId = null;
let cursor = null;
while (true) {
const page = await github.graphql(`
query($owner:String!, $repo:String!, $cursor:String){
repository(owner:$owner, name:$repo){
discussions(first:50, after:$cursor){
nodes{
id
title
url
category{ name }
labels(first:50){ nodes{ name } }
}
pageInfo{ hasNextPage endCursor }
}
}
}
`, { owner, repo, cursor });
const nodes = page.repository.discussions.nodes;
const byLabel = nodes.find(d =>
d.category?.name === categoryName &&
d.labels?.nodes?.some(l => l.name === labelName)
);
if (byLabel) { discussionId = byLabel.id; break; }
const byTitle = nodes.find(d =>
d.category?.name === categoryName &&
d.title === titleTarget
);
if (byTitle) { discussionId = byTitle.id; break; }
if (!page.repository.discussions.pageInfo.hasNextPage) break;
cursor = page.repository.discussions.pageInfo.endCursor;
}
if (!discussionId) {
core.info(`→ Creating discussion for ${labelName}`);
const body =
`**Release date:** TODO (YYYY-MM-DD)\n\n` +
`### Links\n` +
`- [Issues and pull requests marked for version ${version}](https://github.com/${owner}/${repo}/issues?q=label%3A${encodeURIComponent(labelName)})\n`;
const created = await github.graphql(`
mutation($repoId:ID!, $catId:ID!, $title:String!, $body:String!){
createDiscussion(input:{
repositoryId:$repoId,
categoryId:$catId,
title:$title,
body:$body
}){ discussion{ id url } }
}
`, { repoId, catId: categoryId, title: titleTarget, body });
discussionId = created.createDiscussion.discussion.id;
// lock the discussion to prevent replies
await github.graphql(`
mutation($id:ID!){
lockLockable(input:{ lockableId:$id }) {
clientMutationId
}
}
`, { id: discussionId });
core.info(`🔒 Locked discussion ${discussionId}`);
} else {
core.info(`→ Found existing discussion for ${labelName}`);
}
// ensure label exists
let labelId;
try {
await github.rest.issues.getLabel({ owner, repo, name: labelName });
} catch (e) {
if (e.status === 404) {
await github.rest.issues.createLabel({
owner, repo, name: labelName, color: "0E8A16"
});
} else { throw e; }
}
const labelNode = await github.graphql(`
query($owner:String!, $repo:String!, $name:String!){
repository(owner:$owner, name:$repo){ label(name:$name){ id } }
}
`, { owner, repo, name: labelName });
labelId = labelNode.repository.label?.id;
if (!labelId) continue;
// add label to discussion
await github.graphql(`
mutation($id:ID!, $labels:[ID!]!){
addLabelsToLabelable(input:{ labelableId:$id, labelIds:$labels }) {
clientMutationId
}
}
`, { id: discussionId, labels: [labelId] });
core.info(`✓ ${labelName} attached to discussion`);
}
}
}
+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"
-3
View File
@@ -14,14 +14,11 @@
<ItemGroup>
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation" Version="9.0.4" />
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="4.13.0" />
<PackageVersion Include="Microsoft.CodeAnalysis.Common" Version="4.13.0" />
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="4.13.0" />
<PackageVersion Include="Microsoft.CodeAnalysis.Workspaces.Common" Version="4.13.0" />
<PackageVersion Include="Microsoft.CodeAnalysis.Workspaces.MSBuild" Version="4.13.0" />
<PackageVersion Include="Microsoft.Data.Sqlite" Version="9.0.4" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="9.0.4" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.SqlServer" Version="9.0.4" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.4" />
<PackageVersion Include="Microsoft.Extensions.Caching.Abstractions" Version="9.0.4" />
<PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="9.0.4" />
<PackageVersion Include="Microsoft.Extensions.Configuration.Abstractions" Version="9.0.4" />
+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
-1
View File
@@ -51,7 +51,6 @@ steps:
# Test
- pwsh: ${{ parameters.testCommand }}
displayName: Run Playwright tests
continueOnError: true
workingDirectory: tests/Umbraco.Tests.AcceptanceTest
env:
CI: 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: |
+24 -6
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
@@ -526,6 +527,23 @@ stages:
CONNECTIONSTRINGS__UMBRACODBDSN: Data Source=(localdb)\MSSQLLocalDB;AttachDbFilename=|DataDirectory|\Umbraco.mdf;Integrated Security=True
CONNECTIONSTRINGS__UMBRACODBDSN_PROVIDERNAME: Microsoft.Data.SqlClient
additionalEnvironmentVariables: true
# ExtensionRegistry
WindowsExtensionRegistry:
vmImage: "windows-latest"
testFolder: "ExtensionRegistry"
port: ''
testCommand: "npx playwright test --project=extensionRegistry"
CONNECTIONSTRINGS__UMBRACODBDSN: Data Source=(localdb)\MSSQLLocalDB;AttachDbFilename=|DataDirectory|\Umbraco.mdf;Integrated Security=True
CONNECTIONSTRINGS__UMBRACODBDSN_PROVIDERNAME: Microsoft.Data.SqlClient
additionalEnvironmentVariables: false
LinuxExtensionRegistry:
vmImage: "ubuntu-latest"
testFolder: "ExtensionRegistry"
port: ''
testCommand: "npx playwright test --project=extensionRegistry"
CONNECTIONSTRINGS__UMBRACODBDSN: Server=(local);Database=Umbraco;User Id=sa;Password=$(SA_PASSWORD);Encrypt=True;TrustServerCertificate=True
CONNECTIONSTRINGS__UMBRACODBDSN_PROVIDERNAME: Microsoft.Data.SqlClient
additionalEnvironmentVariables: false
pool:
vmImage: $(vmImage)
steps:
+1 -1
View File
@@ -8,8 +8,8 @@ schedules:
displayName: Daily midnight build
branches:
include:
- v10/dev
- v13/dev
- v17/dev
- main
steps:
+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;
}
}
@@ -42,7 +42,7 @@ public class ConfigureUmbracoSwaggerGenOptions : IConfigureOptions<SwaggerGenOpt
swaggerGenOptions.DocInclusionPredicate((name, api) =>
{
if (api.ActionDescriptor is ControllerActionDescriptor controllerActionDescriptor
&& controllerActionDescriptor.MethodInfo.HasMapToApiAttribute(name))
&& controllerActionDescriptor.HasMapToApiAttribute(name))
{
return true;
}
@@ -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,36 @@
using Microsoft.AspNetCore.Mvc.Abstractions;
using Umbraco.Cms.Api.Common.Attributes;
using Umbraco.Cms.Api.Common.Configuration;
namespace Umbraco.Extensions;
/// <summary>
/// Provides extension methods for <see cref="ActionDescriptor"/> to work with <see cref="MapToApiAttribute"/>.
/// </summary>
public static class ActionDescriptorApiCommonExtensions
{
/// <summary>
/// Determines whether the <see cref="ActionDescriptor"/> has a <see cref="MapToApiAttribute"/> with the specified API name.
/// The check is made in runtime to support attributes added in runtime.
/// </summary>
/// <param name="actionDescriptor">The action descriptor to inspect.</param>
/// <param name="apiName">The API name to check for.</param>
/// <returns>
/// <c>true</c> if the <see cref="MapToApiAttribute"/> is present and matches the specified API name,
/// or if the attribute is not present and the API name matches the default API name; otherwise, <c>false</c>.
/// </returns>
public static bool HasMapToApiAttribute(this ActionDescriptor actionDescriptor, string apiName)
{
var value = actionDescriptor.GetMapToApiAttributeValue();
return value == apiName
|| (value is null && apiName == DefaultApiConfiguration.ApiName);
}
private static string? GetMapToApiAttributeValue(this ActionDescriptor actionDescriptor)
{
IEnumerable<MapToApiAttribute> mapToApiAttributes = actionDescriptor?.EndpointMetadata?.OfType<MapToApiAttribute>() ?? [];
return mapToApiAttributes.SingleOrDefault()?.ApiName;
}
}
@@ -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,9 +1,8 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<Title>Umbraco CMS - Delivery API</Title>
<Description>Contains the presentation layer for the Umbraco CMS Delivery API.</Description>
</PropertyGroup>
<PropertyGroup>
<!--
TODO: Fix and remove overrides:
@@ -12,7 +11,6 @@
-->
<WarningsNotAsErrors>$(WarningsNotAsErrors),ASP0019,CS0618,CS0612</WarningsNotAsErrors>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Umbraco.Cms.Api.Common\Umbraco.Cms.Api.Common.csproj" />
<ProjectReference Include="..\Umbraco.Web.Common\Umbraco.Web.Common.csproj" />
@@ -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,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;
}
}
@@ -1,4 +1,4 @@
using System.Text;
using System.Text;
using Microsoft.AspNetCore.Html;
using Microsoft.AspNetCore.Mvc.Rendering;
using Umbraco.Cms.Core;
@@ -24,29 +24,18 @@ public static class HtmlHelperBackOfficeExtensions
IBackOfficePathGenerator backOfficePathGenerator,
IPackageManifestService packageManifestService)
{
try
{
PackageManifestImportmap packageImports = await packageManifestService.GetPackageManifestImportmapAsync();
PackageManifestImportmap packageImports = await packageManifestService.GetPackageManifestImportmapAsync();
var sb = new StringBuilder();
sb.AppendLine("""<script type="importmap">""");
sb.AppendLine(jsonSerializer.Serialize(packageImports));
sb.AppendLine("</script>");
var sb = new StringBuilder();
sb.AppendLine("""<script type="importmap">""");
sb.AppendLine(jsonSerializer.Serialize(packageImports));
sb.AppendLine("</script>");
// Inject the BackOffice cache buster into the import string to handle BackOffice assets
var importmapScript = sb.ToString()
.Replace(backOfficePathGenerator.BackOfficeVirtualDirectory, backOfficePathGenerator.BackOfficeAssetsPath)
.Replace(Constants.Web.CacheBusterToken, backOfficePathGenerator.BackOfficeCacheBustHash);
// Inject the BackOffice cache buster into the import string to handle BackOffice assets
var importmapScript = sb.ToString()
.Replace(backOfficePathGenerator.BackOfficeVirtualDirectory, backOfficePathGenerator.BackOfficeAssetsPath)
.Replace(Constants.Web.CacheBusterToken, backOfficePathGenerator.BackOfficeCacheBustHash);
return html.Raw(importmapScript);
}
catch (NotSupportedException ex)
{
throw new NotSupportedException("Failed to serialize the BackOffice import map", ex);
}
catch (Exception ex)
{
throw new Exception("Failed to generate the BackOffice import map", ex);
}
return html.Raw(importmapScript);
}
}
@@ -1,4 +1,5 @@
using Umbraco.Cms.Core.Mapping;
using Umbraco.Cms.Core.Extensions;
using Umbraco.Cms.Core.Mapping;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Api.Management.Models;
@@ -23,8 +24,7 @@ public class DictionaryPresentationFactory : IDictionaryPresentationFactory
{
DictionaryItemResponseModel dictionaryResponseModel = _umbracoMapper.Map<DictionaryItemResponseModel>(dictionaryItem)!;
var validLanguageIsoCodes = (await _languageService.GetAllAsync())
.Select(language => language.IsoCode)
var validLanguageIsoCodes = (await _languageService.GetAllIsoCodesAsync())
.ToArray();
IDictionaryTranslation[] validTranslations = dictionaryItem.Translations
.Where(t => validLanguageIsoCodes.Contains(t.LanguageIsoCode))
@@ -211,11 +211,7 @@ public class UserGroupPresentationFactory : IUserGroupPresentationFactory
private async Task<Attempt<IEnumerable<string>, UserGroupOperationStatus>> MapLanguageIdsToIsoCodeAsync(IEnumerable<int> ids)
{
IEnumerable<ILanguage> languages = await _languageService.GetAllAsync();
string[] isoCodes = languages
.Where(x => ids.Contains(x.Id))
.Select(x => x.IsoCode)
.ToArray();
string[] isoCodes = await _languageService.GetIsoCodesByIdsAsync(ids.ToArray());
// if a language id does not exist, it simply not returned.
// We do this so we don't have to clean up user group data when deleting languages and to make it easier to restore accidentally removed languages
@@ -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)
+588 -5
View File
@@ -11098,6 +11098,75 @@
]
}
},
"/umbraco/management/api/v1/recycle-bin/document/siblings": {
"get": {
"tags": [
"Document"
],
"operationId": "GetRecycleBinDocumentSiblings",
"parameters": [
{
"name": "target",
"in": "query",
"schema": {
"type": "string",
"format": "uuid"
}
},
{
"name": "before",
"in": "query",
"schema": {
"type": "integer",
"format": "int32"
}
},
{
"name": "after",
"in": "query",
"schema": {
"type": "integer",
"format": "int32"
}
},
{
"name": "dataTypeId",
"in": "query",
"schema": {
"type": "string",
"format": "uuid"
}
}
],
"responses": {
"200": {
"description": "OK",
"content": {
"application/json": {
"schema": {
"oneOf": [
{
"$ref": "#/components/schemas/SubsetDocumentRecycleBinItemResponseModel"
}
]
}
}
}
},
"401": {
"description": "The resource is protected and requires an authentication token"
},
"403": {
"description": "The authenticated user does not have access to this resource"
}
},
"security": [
{
"Backoffice-User": [ ]
}
]
}
},
"/umbraco/management/api/v1/tree/document/ancestors": {
"get": {
"tags": [
@@ -18458,6 +18527,75 @@
]
}
},
"/umbraco/management/api/v1/recycle-bin/media/siblings": {
"get": {
"tags": [
"Media"
],
"operationId": "GetRecycleBinMediaSiblings",
"parameters": [
{
"name": "target",
"in": "query",
"schema": {
"type": "string",
"format": "uuid"
}
},
{
"name": "before",
"in": "query",
"schema": {
"type": "integer",
"format": "int32"
}
},
{
"name": "after",
"in": "query",
"schema": {
"type": "integer",
"format": "int32"
}
},
{
"name": "dataTypeId",
"in": "query",
"schema": {
"type": "string",
"format": "uuid"
}
}
],
"responses": {
"200": {
"description": "OK",
"content": {
"application/json": {
"schema": {
"oneOf": [
{
"$ref": "#/components/schemas/SubsetMediaRecycleBinItemResponseModel"
}
]
}
}
}
},
"401": {
"description": "The resource is protected and requires an authentication token"
},
"403": {
"description": "The authenticated user does not have access to this resource"
}
},
"security": [
{
"Backoffice-User": [ ]
}
]
}
},
"/umbraco/management/api/v1/tree/media/ancestors": {
"get": {
"tags": [
@@ -20207,6 +20345,67 @@
]
}
},
"/umbraco/management/api/v1/tree/member-type/siblings": {
"get": {
"tags": [
"Member Type"
],
"operationId": "GetTreeMemberTypeSiblings",
"parameters": [
{
"name": "target",
"in": "query",
"schema": {
"type": "string",
"format": "uuid"
}
},
{
"name": "before",
"in": "query",
"schema": {
"type": "integer",
"format": "int32"
}
},
{
"name": "after",
"in": "query",
"schema": {
"type": "integer",
"format": "int32"
}
}
],
"responses": {
"200": {
"description": "OK",
"content": {
"application/json": {
"schema": {
"oneOf": [
{
"$ref": "#/components/schemas/SubsetMemberTypeTreeItemResponseModel"
}
]
}
}
}
},
"401": {
"description": "The resource is protected and requires an authentication token"
},
"403": {
"description": "The authenticated user does not have access to this resource"
}
},
"security": [
{
"Backoffice-User": [ ]
}
]
}
},
"/umbraco/management/api/v1/filter/member": {
"get": {
"tags": [
@@ -23625,6 +23824,66 @@
]
}
},
"/umbraco/management/api/v1/tree/partial-view/siblings": {
"get": {
"tags": [
"Partial View"
],
"operationId": "GetTreePartialViewSiblings",
"parameters": [
{
"name": "path",
"in": "query",
"schema": {
"type": "string"
}
},
{
"name": "before",
"in": "query",
"schema": {
"type": "integer",
"format": "int32"
}
},
{
"name": "after",
"in": "query",
"schema": {
"type": "integer",
"format": "int32"
}
}
],
"responses": {
"200": {
"description": "OK",
"content": {
"application/json": {
"schema": {
"oneOf": [
{
"$ref": "#/components/schemas/SubsetFileSystemTreeItemPresentationModel"
}
]
}
}
}
},
"401": {
"description": "The resource is protected and requires an authentication token"
},
"403": {
"description": "The authenticated user does not have access to this resource"
}
},
"security": [
{
"Backoffice-User": [ ]
}
]
}
},
"/umbraco/management/api/v1/preview": {
"delete": {
"tags": [
@@ -25615,6 +25874,66 @@
]
}
},
"/umbraco/management/api/v1/tree/script/siblings": {
"get": {
"tags": [
"Script"
],
"operationId": "GetTreeScriptSiblings",
"parameters": [
{
"name": "path",
"in": "query",
"schema": {
"type": "string"
}
},
{
"name": "before",
"in": "query",
"schema": {
"type": "integer",
"format": "int32"
}
},
{
"name": "after",
"in": "query",
"schema": {
"type": "integer",
"format": "int32"
}
}
],
"responses": {
"200": {
"description": "OK",
"content": {
"application/json": {
"schema": {
"oneOf": [
{
"$ref": "#/components/schemas/SubsetFileSystemTreeItemPresentationModel"
}
]
}
}
}
},
"401": {
"description": "The resource is protected and requires an authentication token"
},
"403": {
"description": "The authenticated user does not have access to this resource"
}
},
"security": [
{
"Backoffice-User": [ ]
}
]
}
},
"/umbraco/management/api/v1/searcher": {
"get": {
"tags": [
@@ -27701,6 +28020,66 @@
]
}
},
"/umbraco/management/api/v1/tree/stylesheet/siblings": {
"get": {
"tags": [
"Stylesheet"
],
"operationId": "GetTreeStylesheetSiblings",
"parameters": [
{
"name": "path",
"in": "query",
"schema": {
"type": "string"
}
},
{
"name": "before",
"in": "query",
"schema": {
"type": "integer",
"format": "int32"
}
},
{
"name": "after",
"in": "query",
"schema": {
"type": "integer",
"format": "int32"
}
}
],
"responses": {
"200": {
"description": "OK",
"content": {
"application/json": {
"schema": {
"oneOf": [
{
"$ref": "#/components/schemas/SubsetFileSystemTreeItemPresentationModel"
}
]
}
}
}
},
"401": {
"description": "The resource is protected and requires an authentication token"
},
"403": {
"description": "The authenticated user does not have access to this resource"
}
},
"security": [
{
"Backoffice-User": [ ]
}
]
}
},
"/umbraco/management/api/v1/tag": {
"get": {
"tags": [
@@ -29711,6 +30090,92 @@
"Backoffice-User": [ ]
}
]
},
"delete": {
"tags": [
"User Data"
],
"operationId": "DeleteUserDataById",
"parameters": [
{
"name": "id",
"in": "path",
"required": true,
"schema": {
"type": "string",
"format": "uuid"
}
}
],
"responses": {
"200": {
"description": "OK",
"headers": {
"Umb-Notifications": {
"description": "The list of notifications produced during the request.",
"schema": {
"type": "array",
"items": {
"$ref": "#/components/schemas/NotificationHeaderModel"
},
"nullable": true
}
}
}
},
"400": {
"description": "Bad Request",
"headers": {
"Umb-Notifications": {
"description": "The list of notifications produced during the request.",
"schema": {
"type": "array",
"items": {
"$ref": "#/components/schemas/NotificationHeaderModel"
},
"nullable": true
}
}
},
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/UserDataOperationStatusModel"
}
}
}
},
"404": {
"description": "Not Found",
"headers": {
"Umb-Notifications": {
"description": "The list of notifications produced during the request.",
"schema": {
"type": "array",
"items": {
"$ref": "#/components/schemas/NotificationHeaderModel"
},
"nullable": true
}
}
},
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/UserDataOperationStatusModel"
}
}
}
},
"401": {
"description": "The resource is protected and requires an authentication token"
}
},
"security": [
{
"Backoffice-User": [ ]
}
]
}
},
"/umbraco/management/api/v1/filter/user-group": {
@@ -37985,7 +38450,8 @@
"type": "boolean"
},
"allowNonExistingSegmentsCreation": {
"type": "boolean"
"type": "boolean",
"deprecated": true
}
},
"additionalProperties": false
@@ -39254,7 +39720,8 @@
"NotCreated",
"Draft",
"Published",
"PublishedPendingChanges"
"PublishedPendingChanges",
"Trashed"
],
"type": "string"
},
@@ -39749,7 +40216,7 @@
},
"actionParameters": {
"type": "object",
"additionalProperties": { },
"additionalProperties": {},
"nullable": true
}
},
@@ -40374,7 +40841,7 @@
},
"extensions": {
"type": "array",
"items": { }
"items": {}
}
},
"additionalProperties": false
@@ -44211,7 +44678,7 @@
"nullable": true
}
},
"additionalProperties": { }
"additionalProperties": {}
},
"ProblemDetailsBuilderModel": {
"type": "object",
@@ -45374,6 +45841,35 @@
},
"additionalProperties": false
},
"SubsetDocumentRecycleBinItemResponseModel": {
"required": [
"items",
"totalAfter",
"totalBefore"
],
"type": "object",
"properties": {
"totalBefore": {
"type": "integer",
"format": "int64"
},
"totalAfter": {
"type": "integer",
"format": "int64"
},
"items": {
"type": "array",
"items": {
"oneOf": [
{
"$ref": "#/components/schemas/DocumentRecycleBinItemResponseModel"
}
]
}
}
},
"additionalProperties": false
},
"SubsetDocumentTreeItemResponseModel": {
"required": [
"items",
@@ -45432,6 +45928,64 @@
},
"additionalProperties": false
},
"SubsetFileSystemTreeItemPresentationModel": {
"required": [
"items",
"totalAfter",
"totalBefore"
],
"type": "object",
"properties": {
"totalBefore": {
"type": "integer",
"format": "int64"
},
"totalAfter": {
"type": "integer",
"format": "int64"
},
"items": {
"type": "array",
"items": {
"oneOf": [
{
"$ref": "#/components/schemas/FileSystemTreeItemPresentationModel"
}
]
}
}
},
"additionalProperties": false
},
"SubsetMediaRecycleBinItemResponseModel": {
"required": [
"items",
"totalAfter",
"totalBefore"
],
"type": "object",
"properties": {
"totalBefore": {
"type": "integer",
"format": "int64"
},
"totalAfter": {
"type": "integer",
"format": "int64"
},
"items": {
"type": "array",
"items": {
"oneOf": [
{
"$ref": "#/components/schemas/MediaRecycleBinItemResponseModel"
}
]
}
}
},
"additionalProperties": false
},
"SubsetMediaTreeItemResponseModel": {
"required": [
"items",
@@ -45490,6 +46044,35 @@
},
"additionalProperties": false
},
"SubsetMemberTypeTreeItemResponseModel": {
"required": [
"items",
"totalAfter",
"totalBefore"
],
"type": "object",
"properties": {
"totalBefore": {
"type": "integer",
"format": "int64"
},
"totalAfter": {
"type": "integer",
"format": "int64"
},
"items": {
"type": "array",
"items": {
"oneOf": [
{
"$ref": "#/components/schemas/MemberTypeTreeItemResponseModel"
}
]
}
}
},
"additionalProperties": false
},
"SubsetNamedEntityTreeItemResponseModel": {
"required": [
"items",
@@ -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");
}
@@ -8,5 +8,6 @@ public class DocumentConfigurationResponseModel
public required bool AllowEditInvariantFromNonDefault { get; set; }
[Obsolete("This functionality will be moved to a client-side extension. Scheduled for removal in V19.")]
public required bool AllowNonExistingSegmentsCreation { get; set; }
}
@@ -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,
}
@@ -7,7 +7,7 @@
<PropertyGroup>
<!--
TODO: Fix and remove overrides:
[SA1405] Debug assret message text
[SA1405] Debug assert message text
[SA1121] resolve hiding inherited members
[SA1117] remove async or make method synchronous
[IDE1006] fix naming rule violation
@@ -3,7 +3,7 @@
<Title>Umbraco CMS - Persistence - SQLite</Title>
<Description>Adds support for SQLite to Umbraco CMS.</Description>
</PropertyGroup>
<PropertyGroup>
<!--
TODO: Fix and remove overrides:
@@ -11,7 +11,6 @@
-->
<WarningsNotAsErrors>$(WarningsNotAsErrors),CS0114</WarningsNotAsErrors>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Data.Sqlite" />
</ItemGroup>
@@ -5,7 +5,6 @@
<IncludeBuildOutput>false</IncludeBuildOutput>
<IncludeSymbols>false</IncludeSymbols>
</PropertyGroup>
<PropertyGroup>
<!--
TODO: Fix and remove overrides:
@@ -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;
}
@@ -181,6 +181,11 @@ public class GlobalSettings
/// </summary>
public bool IsSmtpServerConfigured => !string.IsNullOrWhiteSpace(Smtp?.Host);
/// <summary>
/// Gets a value indicating whether SMTP expiry is configured.
/// </summary>
public bool IsSmtpExpiryConfigured => Smtp?.EmailExpiration != null && Smtp?.EmailExpiration.HasValue == true;
/// <summary>
/// Gets a value indicating whether there is a physical pickup directory configured.
/// </summary>
@@ -34,6 +34,9 @@ public class SecuritySettings
internal const string StaticAuthorizeCallbackLogoutPathName = "/umbraco/logout";
internal const string StaticAuthorizeCallbackErrorPathName = "/umbraco/error";
internal const string StaticPasswordResetEmailExpiry = "01:00:00";
internal const string StaticUserInviteEmailExpiry = "3.00:00:00";
/// <summary>
/// Gets or sets a value indicating whether to keep the user logged in.
/// </summary>
@@ -159,4 +162,16 @@ public class SecuritySettings
/// </summary>
[DefaultValue(StaticAuthorizeCallbackErrorPathName)]
public string AuthorizeCallbackErrorPathName { get; set; } = StaticAuthorizeCallbackErrorPathName;
/// <summary>
/// Gets or sets the expiry time for password reset emails.
/// </summary>
[DefaultValue(StaticPasswordResetEmailExpiry)]
public TimeSpan PasswordResetEmailExpiry { get; set; } = TimeSpan.Parse(StaticPasswordResetEmailExpiry);
/// <summary>
/// Gets or sets the expiry time for user invite emails.
/// </summary>
[DefaultValue(StaticUserInviteEmailExpiry)]
public TimeSpan UserInviteEmailExpiry { get; set; } = TimeSpan.Parse(StaticUserInviteEmailExpiry);
}
@@ -19,5 +19,6 @@ public class SegmentSettings
/// <summary>
/// Gets or sets a value indicating whether the creation of non-existing segments is allowed.
/// </summary>
[Obsolete("This functionality will be moved to a client-side extension. Scheduled for removal in V19.")]
public bool AllowCreation { get; set; } = StaticAllowCreation;
}
@@ -96,4 +96,9 @@ public class SmtpSettings : ValidatableEntryBase
/// Gets or sets a value for the SMTP password.
/// </summary>
public string? Password { get; set; }
/// <summary>
/// Gets or sets a value for the time until an email expires.
/// </summary>
public TimeSpan? EmailExpiration { get; set; }
}
@@ -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
{
@@ -45,7 +45,7 @@ public abstract class ApiContentBuilderBase<T>
public virtual T? Build(IPublishedContent content)
{
IApiContentRoute? route = ApiContentRouteBuilder.Build(content);
IApiContentRoute? route = ApiContentRouteBuilder.Build(content, VariationContextAccessor.VariationContext?.Culture);
if (route is null)
{
return default;
@@ -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();
@@ -0,0 +1,16 @@
using Umbraco.Cms.Core.Services;
namespace Umbraco.Cms.Core.Extensions;
/// <summary>
/// Provides extension methods for <see cref="ILanguageService"/>.
/// </summary>
public static class LanguageServiceExtensions
{
/// <summary>
/// Retrieves all ISO codes of all languages.
/// </summary>
/// <param name="service">The <see cref="ILanguageService"/>.</param>
/// <returns>A collection of language ISO codes.</returns>
public static async Task<IEnumerable<string>> GetAllIsoCodesAsync(this ILanguageService service) => (await service.GetAllAsync()).Select(x => x.IsoCode);
}
@@ -74,7 +74,7 @@ public class EmailNotificationMethod : NotificationMethodBase
var subject = _textService?.Localize("healthcheck", "scheduledHealthCheckEmailSubject", new[] { host });
EmailMessage mailMessage = CreateMailMessage(subject, message);
Task? task = _emailSender?.SendAsync(mailMessage, Constants.Web.EmailTypes.HealthCheck);
Task? task = _emailSender?.SendAsync(mailMessage, Constants.Web.EmailTypes.HealthCheck, false, null);
if (task is not null)
{
await task;
+22 -8
View File
@@ -2,7 +2,7 @@ using System.Text.RegularExpressions;
namespace Umbraco.Cms.Core.IO;
internal sealed class ShadowFileSystem : IFileSystem
internal sealed partial class ShadowFileSystem : IFileSystem
{
private readonly IFileSystem _sfs;
@@ -392,14 +392,28 @@ internal sealed class ShadowFileSystem : IFileSystem
}
// copied from System.Web.Util.Wildcard internal
internal sealed class WildcardExpression
internal sealed partial class WildcardExpression
{
private static readonly Regex MetaRegex = new("[\\+\\{\\\\\\[\\|\\(\\)\\.\\^\\$]");
private static readonly Regex QuestRegex = new("\\?");
private static readonly Regex StarRegex = new("\\*");
private static readonly Regex CommaRegex = new(",");
private static readonly Regex SlashRegex = new("(?=/)");
private static readonly Regex BackslashRegex = new("(?=[\\\\:])");
private static readonly Regex MetaRegex = GetMetaRegex();
[GeneratedRegex("[\\+\\{\\\\\\[\\|\\(\\)\\.\\^\\$]")]
private static partial Regex GetMetaRegex();
private static readonly Regex QuestRegex = GetQuestRegex();
[GeneratedRegex("\\?")]
private static partial Regex GetQuestRegex();
private static readonly Regex StarRegex = GetStarRegex();
[GeneratedRegex("\\*")]
private static partial Regex GetStarRegex();
private static readonly Regex CommaRegex = GetCommaRegex();
[GeneratedRegex(",")]
private static partial Regex GetCommaRegex();
private readonly bool _caseInsensitive;
private readonly string _pattern;
private Regex? _regex;
+19
View File
@@ -7,9 +7,28 @@ namespace Umbraco.Cms.Core.Mail;
/// </summary>
public interface IEmailSender
{
/// <summary>
/// Sends a message asynchronously.
/// </summary>
[Obsolete("Please use the overload with expires parameter. Scheduled for removal in Umbraco 18.")]
Task SendAsync(EmailMessage message, string emailType);
/// <summary>
/// Sends a message asynchronously.
/// </summary>
[Obsolete("Please use the overload with expires parameter. Scheduled for removal in Umbraco 18.")]
Task SendAsync(EmailMessage message, string emailType, bool enableNotification);
/// <summary>
/// Sends a message asynchronously.
/// </summary>
Task SendAsync(EmailMessage message, string emailType, bool enableNotification = false, TimeSpan? expires = null)
#pragma warning disable CS0618 // Type or member is obsolete
=> SendAsync(message, emailType, enableNotification);
#pragma warning restore CS0618 // Type or member is obsolete
/// <summary>
/// Verifies if the email sender is configured to send emails.
/// </summary>
bool CanSendRequiredEmail();
}
@@ -12,6 +12,10 @@ internal sealed class NotImplementedEmailSender : IEmailSender
throw new NotImplementedException(
"To send an Email ensure IEmailSender is implemented with a custom implementation");
public Task SendAsync(EmailMessage message, string emailType, bool enableNotification, TimeSpan? expires) =>
throw new NotImplementedException(
"To send an Email ensure IEmailSender is implemented with a custom implementation");
public bool CanSendRequiredEmail()
=> throw new NotImplementedException(
"To send an Email ensure IEmailSender is implemented with a custom implementation");
+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
}
+3
View File
@@ -24,6 +24,9 @@ public interface IPropertyType : IEntity, IRememberBeingDirty
/// </summary>
int DataTypeId { get; set; }
/// <summary>
/// Gets or sets the Guid unique identifier of the datatype for this property type.
/// </summary>
Guid DataTypeKey { get; set; }
/// <summary>
+2
View File
@@ -47,6 +47,7 @@ public class PropertyType : EntityBase, IPropertyType, IEquatable<PropertyType>
if (dataType.HasIdentity)
{
_dataTypeId = dataType.Id;
_dataTypeKey = dataType.Key;
}
_propertyEditorAlias = dataType.EditorAlias;
@@ -159,6 +160,7 @@ public class PropertyType : EntityBase, IPropertyType, IEquatable<PropertyType>
set => SetPropertyValueAndDetectChanges(value, ref _dataTypeId, nameof(DataTypeId));
}
/// <inheritdoc />
[DataMember]
public Guid DataTypeKey
{
@@ -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>
@@ -38,5 +38,11 @@ public interface ILanguageRepository : IReadWriteQueryRepository<int, ILanguage>
/// </remarks>
int? GetDefaultId();
/// <summary>
/// Gets multiple language ISO codes from the provided Ids.
/// </summary>
/// <param name="ids">The language Ids.</param>
/// <param name="throwOnNotFound">Indicates whether to throw an exception if the provided Id is not found as a language.</param>
/// <returns></returns>
string[] GetIsoCodesByIds(ICollection<int> ids, bool throwOnNotFound = true);
}
@@ -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);
}
@@ -1,4 +1,5 @@
using Microsoft.Extensions.Logging;
using Umbraco.Cms.Core.Extensions;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.PublishedContent;
using Umbraco.Cms.Core.Services;
@@ -42,7 +43,7 @@ public class PublishedUrlInfoProvider : IPublishedUrlInfoProvider
public async Task<ISet<UrlInfo>> GetAllAsync(IContent content)
{
HashSet<UrlInfo> urlInfos = [];
var cultures = (await _languageService.GetAllAsync()).Select(x => x.IsoCode).ToArray();
IEnumerable<string> cultures = await _languageService.GetAllIsoCodesAsync();
// First we get the urls of all cultures, using the published router, meaning we respect any extensions.
foreach (var culture in cultures)
@@ -2,6 +2,7 @@ using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Core.Extensions;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.PublishedContent;
using Umbraco.Cms.Core.PublishedCache;
@@ -118,7 +119,7 @@ public static class UrlProviderExtensions
// and, not only for those assigned to domains in the branch, because we want
// to show what GetUrl() would return, for every culture.
var urls = new HashSet<UrlInfo>();
var cultures = (await languageService.GetAllAsync()).Select(x => x.IsoCode).ToList();
IEnumerable<string> cultures = await languageService.GetAllIsoCodesAsync();
// get all URLs for all cultures
// in a HashSet, so de-duplicates too
@@ -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();
}
}
@@ -1,6 +1,7 @@
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Umbraco.Cms.Core.Configuration.Models;
using Umbraco.Cms.Core.Extensions;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.ContentEditing;
using Umbraco.Cms.Core.Models.Membership;
@@ -104,8 +105,8 @@ internal sealed class ContentEditingService
{
// If no cultures are provided, we are asking to validate all cultures. But if the user doesn't have access to all, we
// should only validate the ones they do.
var allCultures = (await _languageService.GetAllAsync()).Select(x => x.IsoCode).ToList();
return allowedCultures.Count == allCultures.Count ? null : allowedCultures;
IEnumerable<string> allCultures = await _languageService.GetAllIsoCodesAsync();
return allowedCultures.Count == allCultures.Count() ? null : allowedCultures;
}
// If explicit cultures are provided, we should only validate the ones the user has access to.
@@ -2,11 +2,13 @@ using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Umbraco.Cms.Core.Configuration.Models;
using Umbraco.Cms.Core.Events;
using Umbraco.Cms.Core.Extensions;
using Umbraco.Cms.Core.Models;
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;
@@ -25,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,
@@ -36,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;
@@ -52,6 +56,7 @@ internal sealed class ContentPublishingService : IContentPublishingService
{
_contentSettings = contentSettings;
});
_umbracoContextFactory = umbracoContextFactory;
}
/// <inheritdoc />
@@ -165,7 +170,7 @@ internal sealed class ContentPublishingService : IContentPublishingService
return Attempt.FailWithStatus(ContentPublishingOperationStatus.CannotPublishInvariantWhenVariant, new ContentPublishingResult());
}
IEnumerable<string> validCultures = (await _languageService.GetAllAsync()).Select(x => x.IsoCode);
IEnumerable<string> validCultures = await _languageService.GetAllIsoCodesAsync();
if (validCultures.ContainsAll(cultures) is false)
{
scope.Complete();
@@ -289,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),
@@ -323,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)
@@ -488,7 +497,7 @@ internal sealed class ContentPublishingService : IContentPublishingService
return Attempt.Fail(ContentPublishingOperationStatus.CannotPublishVariantWhenNotVariant);
}
var validCultures = (await _languageService.GetAllAsync()).Select(x => x.IsoCode).ToArray();
var validCultures = (await _languageService.GetAllIsoCodesAsync()).ToArray();
foreach (var culture in cultures)
{
+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();
}
@@ -1,4 +1,4 @@
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.ContentTypeEditing;
using Umbraco.Cms.Core.Services.OperationStatus;
using Umbraco.Cms.Core.Strings;
@@ -407,7 +407,7 @@ internal abstract class ContentTypeEditingServiceBase<TContentType, TContentType
}
// This this method gets aliases across documents, members, and media, so it covers it all
private bool ContentTypeAliasIsInUse(string alias) => _contentTypeService.GetAllContentTypeAliases().Contains(alias);
private bool ContentTypeAliasIsInUse(string alias) => _contentTypeService.GetAllContentTypeAliases().InvariantContains(alias);
private bool ContentTypeAliasCanBeUsedFor(string alias, Guid key)
{
@@ -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;
@@ -1,4 +1,5 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations;
using Umbraco.Cms.Core.Extensions;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.ContentEditing;
using Umbraco.Cms.Core.Models.ContentEditing.Validation;
@@ -137,7 +138,7 @@ internal abstract class ContentValidationServiceBase<TContentType>
return invalidCultures.IsCollectionEmpty();
}
private async Task<string[]> GetCultureCodes() => (await _languageService.GetAllAsync()).Select(language => language.IsoCode).ToArray();
private async Task<string[]> GetCultureCodes() => (await _languageService.GetAllIsoCodesAsync()).ToArray();
private IEnumerable<PropertyValidationError> ValidateProperty(IPropertyType propertyType, PropertyValueModel? propertyValueModel, PropertyValidationContext validationContext)
{
@@ -432,7 +432,8 @@ public class DocumentUrlService : IDocumentUrlService
if (draftUrlSegments.Any() is false)
{
_logger.LogWarning("No draft URL segments found for document {DocumentKey} in culture {Culture}", document.Key, culture ?? "{null}");
// Log at debug level because this is expected when a document is not published in a given language.
_logger.LogDebug("No draft URL segments found for document {DocumentKey} in culture {Culture}", document.Key, culture ?? "{null}");
}
else
{
@@ -856,7 +857,7 @@ public class DocumentUrlService : IDocumentUrlService
.Concat(_contentService.GetAncestors(documentIdAttempt.Result).Select(x => x.Key).Reverse());
IEnumerable<ILanguage> languages = await _languageService.GetAllAsync();
var cultures = languages.ToDictionary(x=>x.IsoCode);
var cultures = languages.ToDictionary(x => x.IsoCode);
Guid[] ancestorsOrSelfKeysArray = ancestorsOrSelfKeys as Guid[] ?? ancestorsOrSelfKeys.ToArray();
Dictionary<Guid, Task<ILookup<string, Domain>>> ancestorOrSelfKeyToDomains = ancestorsOrSelfKeysArray
+11 -11
View File
@@ -1,4 +1,4 @@
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Services.OperationStatus;
namespace Umbraco.Cms.Core.Services;
@@ -32,9 +32,9 @@ public interface ILanguageService
Task<string> GetDefaultIsoCodeAsync();
/// <summary>
/// Gets all available languages
/// Gets all available languages.
/// </summary>
/// <returns>An enumerable list of <see cref="ILanguage" /> objects</returns>
/// <returns>An enumerable list of <see cref="ILanguage" /> objects.</returns>
Task<IEnumerable<ILanguage>> GetAllAsync();
/// <summary>
@@ -44,29 +44,29 @@ public interface ILanguageService
Task<IEnumerable<ILanguage>> GetMultipleAsync(IEnumerable<string> isoCodes);
/// <summary>
/// Updates an existing <see cref="ILanguage" /> object
/// Updates an existing <see cref="ILanguage" /> object.
/// </summary>
/// <param name="language"><see cref="ILanguage" /> to update</param>
/// <param name="userKey">Key of the user saving the language</param>
Task<Attempt<ILanguage, LanguageOperationStatus>> UpdateAsync(ILanguage language, Guid userKey);
/// <summary>
/// Creates a new <see cref="ILanguage" /> object
/// Creates a new <see cref="ILanguage" /> object.
/// </summary>
/// <param name="language"><see cref="ILanguage" /> to create</param>
/// <param name="userKey">Key of the user creating the language</param>
/// <param name="language"><see cref="ILanguage" /> to create.</param>
/// <param name="userKey">Key of the user creating the language.</param>
Task<Attempt<ILanguage, LanguageOperationStatus>> CreateAsync(ILanguage language, Guid userKey);
/// <summary>
/// Deletes a <see cref="ILanguage" /> by removing it and its usages from the db
/// Deletes a <see cref="ILanguage" /> by removing it and its usages from the database.
/// </summary>
/// <param name="isoCode">The ISO code of the <see cref="ILanguage" /> to delete</param>
/// <param name="userKey">Key of the user deleting the language</param>
/// <param name="isoCode">The ISO code of the <see cref="ILanguage" /> to delete.</param>
/// <param name="userKey">Key of the user deleting the language.</param>
Task<Attempt<ILanguage?, LanguageOperationStatus>> DeleteAsync(string isoCode, Guid userKey);
/// <summary>
/// Retrieves the isoCodes of configured languages by their Ids
/// Retrieves the ISO codes of configured languages by their Ids.
/// </summary>
/// <param name="ids">The ids of the configured <see cref="ILanguage" />s</param>
/// <returns>The ISO codes of the <see cref="ILanguage" />s</returns>
@@ -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);
}
@@ -1,4 +1,3 @@
using System.Globalization;
using Microsoft.Extensions.Logging;
using Umbraco.Cms.Core.Events;
using Umbraco.Cms.Core.Models;
@@ -557,7 +557,7 @@ public class NotificationService : INotificationService
{
ThreadPool.QueueUserWorkItem(state =>
{
if (_logger.IsEnabled(Microsoft.Extensions.Logging.LogLevel.Debug))
if (_logger.IsEnabled(LogLevel.Debug))
{
_logger.LogDebug("Begin processing notifications.");
}
@@ -569,9 +569,9 @@ public class NotificationService : INotificationService
{
try
{
_emailSender.SendAsync(request.Mail, Constants.Web.EmailTypes.Notification).GetAwaiter()
_emailSender.SendAsync(request.Mail, Constants.Web.EmailTypes.Notification, false, null).GetAwaiter()
.GetResult();
if (_logger.IsEnabled(Microsoft.Extensions.Logging.LogLevel.Debug))
if (_logger.IsEnabled(LogLevel.Debug))
{
_logger.LogDebug("Notification '{Action}' sent to {Username} ({Email})", request.Action, request.UserName, request.Email);
}
+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)
{
@@ -15,15 +15,15 @@ public sealed class HtmlLocalLinkParser
// <a type="document" href="/{localLink:eed5fc6b-96fd-45a5-a0f1-b1adfb483c2f}" title="other page">other page</a>
internal static readonly Regex LocalLinkTagPattern = new(
@"<a.+?href=['""](?<locallink>\/?{localLink:(?<guid>[a-fA-F0-9-]+)})[^>]*?>",
RegexOptions.IgnoreCase | RegexOptions.IgnorePatternWhitespace | RegexOptions.Singleline);
RegexOptions.IgnoreCase | RegexOptions.IgnorePatternWhitespace | RegexOptions.Singleline | RegexOptions.Compiled);
internal static readonly Regex TypePattern = new(
"""type=['"](?<type>(?:media|document))['"]""",
RegexOptions.IgnoreCase | RegexOptions.IgnorePatternWhitespace);
RegexOptions.IgnoreCase | RegexOptions.IgnorePatternWhitespace | RegexOptions.Compiled);
internal static readonly Regex LocalLinkPattern = new(
@"href=['""](?<locallink>\/?(?:\{|\%7B)localLink:(?<guid>[a-zA-Z0-9-://]+)(?:\}|\%7D))",
RegexOptions.IgnoreCase | RegexOptions.IgnorePatternWhitespace);
RegexOptions.IgnoreCase | RegexOptions.IgnorePatternWhitespace | RegexOptions.Compiled);
private readonly IPublishedUrlProvider _publishedUrlProvider;
+1 -1
View File
@@ -16,7 +16,7 @@
[CS0618]/[CS0672]/[SYSLIB0051]/[SYSLIB0044]/[SYSLIB0023]/[SYSLIB0003]/[SYSLIB0045] adjust obsolete references
[CS0067] unused event
[SA1405] debug provide message text
[CS0168]/[CS0169] unused fields/variables
[CS0168] and [CS0169] unused fields/variables
[CS0183] always of type
[SA1111] adjust parenthesis
[CA2017] match parameters number
@@ -11,6 +11,7 @@ using Microsoft.Extensions.DependencyInjection;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Cache;
using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Core.Extensions;
using Umbraco.Cms.Core.Mapping;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.ContentEditing;
@@ -260,9 +261,7 @@ public class BackOfficeExamineSearcher : IBackOfficeExamineSearcher
// then nodeName will be matched normally with wildcards
// the rest will be normal without wildcards
var allLangs = _languageService.GetAllAsync().GetAwaiter().GetResult()
.Select(x => x.IsoCode.ToLowerInvariant())
.ToList();
var allLangs = _languageService.GetAllIsoCodesAsync().GetAwaiter().GetResult().Select(x => x.ToLowerInvariant()).ToList();
// the chars [*-_] in the query will mess everything up so let's remove those
// However we cannot just remove - and _ since these signify a space, so we instead replace them with that.
@@ -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");
}
@@ -5,7 +5,7 @@
<Description>Adds Examine searching support using Lucene to Umbraco CMS.</Description>
<RootNamespace>Umbraco.Cms.Infrastructure.Examine</RootNamespace>
</PropertyGroup>
<PropertyGroup>
<!--
TODO: Fix and remove overrides:
@@ -22,7 +22,6 @@
<!-- Take top-level depedendency on System.Security.Cryptography.Xml, because Examine depends on a vulnerable version -->
<PackageReference Include="System.Security.Cryptography.Xml" />
<!-- Take top-level depedendency on Lucene.Net.Replicator, because Examine depends on a vulnerable version -->
<!-- Take top-level depedendency on Lucene.Net.Replicator-->
<PackageReference Include="Lucene.Net.Replicator" />
</ItemGroup>
@@ -37,7 +37,8 @@ public class RecurringBackgroundJobHostedServiceRunner : IHostedService
_logger.LogDebug("Creating background hosted service for {job}", jobName);
IHostedService hostedService = _jobFactory(job);
_logger.LogInformation("Starting background hosted service for {job}", jobName);
_logger.LogInformation("Starting a background hosted service for {job} with a delay of {delay}, running every {period}", jobName, job.Delay, job.Period);
await hostedService.StartAsync(cancellationToken).ConfigureAwait(false);
_hostedServices.Add(new NamedServiceJob(jobName, hostedService));
@@ -20,6 +20,7 @@ public static partial class UmbracoBuilderExtensions
// Add post migration notification handlers
builder.AddNotificationHandler<UmbracoPlanExecutedNotification, ClearCsrfCookieHandler>();
return builder;
}
}
@@ -59,7 +59,19 @@ internal sealed class DeliveryApiContentIndexHandleContentChanges : DeliveryApiC
RemoveFromIndex(pendingRemovals, index);
pendingRemovals.Clear();
Reindex(content, index);
ReIndexResult reIndexResult = Reindex(content, index);
// When we get to this point, we are dealing with either
// a refresh node or a refresh branch (see reindex =...).
// A refresh branch can be many things, the Reindex function takes care of most scenarios.
// But it only reindexes descendants if the base node has any changed cultures (see comments in that function)
// So by checking what kind of operation it did when the initial indexrequest is for a refresh branch,
// we can support reindexing a branch while the base node was unchanged.
if (reIndexResult == ReIndexResult.Updated && changeTypes.HasType(TreeChangeTypes.RefreshBranch))
{
ReindexDescendants(content, index);
}
}
}
@@ -68,7 +80,7 @@ internal sealed class DeliveryApiContentIndexHandleContentChanges : DeliveryApiC
return Task.CompletedTask;
});
private void Reindex(IContent content, IIndex index)
private ReIndexResult Reindex(IContent content, IIndex index)
{
// get the currently indexed cultures for the content
CulturePublishStatus[] existingCultures = index
@@ -95,16 +107,19 @@ internal sealed class DeliveryApiContentIndexHandleContentChanges : DeliveryApiC
// we likely got here because a removal triggered a "refresh branch" notification, now we
// need to delete every last culture of this content and all descendants
RemoveFromIndex(content.Id, index);
return;
return ReIndexResult.Removed;
}
// if the published state changed of any culture, chances are there are similar changes ot the content descendants
// if the published state changed of any culture, chances are there are similar changes at the content descendants
// that need to be reflected in the index, so we'll reindex all descendants
var changedCulturePublishStatus = indexedCultures.Intersect(existingCultures).Count() != existingCultures.Length;
if (changedCulturePublishStatus)
{
ReindexDescendants(content, index);
return ReIndexResult.UpdatedWithDescendants;
}
return ReIndexResult.Updated;
}
private CulturePublishStatus[] UpdateIndex(IContent content, IIndex index)
@@ -179,4 +194,11 @@ internal sealed class DeliveryApiContentIndexHandleContentChanges : DeliveryApiC
public override int GetHashCode() => HashCode.Combine(Culture, Published);
}
private enum ReIndexResult
{
Updated,
UpdatedWithDescendants,
Removed,
}
}
@@ -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)
{
@@ -15,28 +15,44 @@ namespace Umbraco.Cms.Infrastructure.Mail
public class BasicSmtpEmailSenderClient : IEmailSenderClient
{
private readonly GlobalSettings _globalSettings;
public BasicSmtpEmailSenderClient(IOptionsMonitor<GlobalSettings> globalSettings)
{
_globalSettings = globalSettings.CurrentValue;
}
/// <inheritdoc />
public BasicSmtpEmailSenderClient(IOptionsMonitor<GlobalSettings> globalSettings)
=> _globalSettings = globalSettings.CurrentValue;
/// <inheritdoc />
public async Task SendAsync(EmailMessage message)
=> await SendAsync(message, null);
/// <inheritdoc />
public async Task SendAsync(EmailMessage message, TimeSpan? expires)
{
using var client = new SmtpClient();
await client.ConnectAsync(
_globalSettings.Smtp!.Host,
_globalSettings.Smtp.Port,
_globalSettings.Smtp.Port,
(SecureSocketOptions)(int)_globalSettings.Smtp.SecureSocketOptions);
if (!string.IsNullOrWhiteSpace(_globalSettings.Smtp.Username) &&
!string.IsNullOrWhiteSpace(_globalSettings.Smtp.Password))
!string.IsNullOrWhiteSpace(_globalSettings.Smtp.Password))
{
await client.AuthenticateAsync(_globalSettings.Smtp.Username, _globalSettings.Smtp.Password);
}
var mimeMessage = message.ToMimeMessage(_globalSettings.Smtp!.From);
if (_globalSettings.IsSmtpExpiryConfigured)
{
expires ??= _globalSettings.Smtp.EmailExpiration;
}
if (expires.HasValue)
{
// `Expires` header needs to be in RFC 1123/2822 compatible format
mimeMessage.Headers.Add("Expires", DateTimeOffset.UtcNow.Add(expires.GetValueOrDefault()).ToString("R"));
}
if (_globalSettings.Smtp.DeliveryMethod == SmtpDeliveryMethod.Network)
{
await client.SendAsync(mimeMessage);
+22 -14
View File
@@ -30,6 +30,9 @@ public class EmailSender : IEmailSender
private GlobalSettings _globalSettings;
private readonly IEmailSenderClient _emailSenderClient;
/// <summary>
/// Initializes a new instance of the <see cref="EmailSender"/> class.
/// </summary>
[Obsolete("Please use the non-obsolete constructor. Will be removed in V17.")]
public EmailSender(
ILogger<EmailSender> logger,
@@ -39,6 +42,9 @@ public class EmailSender : IEmailSender
{
}
/// <summary>
/// Initializes a new instance of the <see cref="EmailSender"/> class.
/// </summary>
[Obsolete("Please use the non-obsolete constructor. Will be removed in V17.")]
public EmailSender(
ILogger<EmailSender> logger,
@@ -55,6 +61,9 @@ public class EmailSender : IEmailSender
globalSettings.OnChange(x => _globalSettings = x);
}
/// <summary>
/// Initializes a new instance of the <see cref="EmailSender"/> class.
/// </summary>
[ActivatorUtilitiesConstructor]
public EmailSender(
ILogger<EmailSender> logger,
@@ -72,19 +81,19 @@ public class EmailSender : IEmailSender
globalSettings.OnChange(x => _globalSettings = x);
}
/// <summary>
/// Sends the message async
/// </summary>
/// <returns></returns>
/// <inheritdoc/>
public async Task SendAsync(EmailMessage message, string emailType) =>
await SendAsyncInternal(message, emailType, false);
await SendAsyncInternal(message, emailType, false, null);
/// <inheritdoc/>
public async Task SendAsync(EmailMessage message, string emailType, bool enableNotification) =>
await SendAsyncInternal(message, emailType, enableNotification);
await SendAsyncInternal(message, emailType, enableNotification, null);
/// <summary>
/// Returns true if the application should be able to send a required application email
/// </summary>
/// <inheritdoc/>
public async Task SendAsync(EmailMessage message, string emailType, bool enableNotification = false, TimeSpan? expires = null) =>
await SendAsyncInternal(message, emailType, enableNotification, expires);
/// <inheritdoc/>
/// <remarks>
/// We assume this is possible if either an event handler is registered or an smtp server is configured
/// or a pickup directory location is configured
@@ -93,7 +102,7 @@ public class EmailSender : IEmailSender
|| _globalSettings.IsPickupDirectoryLocationConfigured
|| _notificationHandlerRegistered;
private async Task SendAsyncInternal(EmailMessage message, string emailType, bool enableNotification)
private async Task SendAsyncInternal(EmailMessage message, string emailType, bool enableNotification, TimeSpan? expires)
{
if (enableNotification)
{
@@ -104,7 +113,7 @@ public class EmailSender : IEmailSender
// if a handler handled sending the email then don't continue.
if (notification.IsHandled)
{
if (_logger.IsEnabled(Microsoft.Extensions.Logging.LogLevel.Debug))
if (_logger.IsEnabled(LogLevel.Debug))
{
_logger.LogDebug(
"The email sending for {Subject} was handled by a notification handler",
@@ -116,7 +125,7 @@ public class EmailSender : IEmailSender
if (!_globalSettings.IsSmtpServerConfigured && !_globalSettings.IsPickupDirectoryLocationConfigured)
{
if (_logger.IsEnabled(Microsoft.Extensions.Logging.LogLevel.Debug))
if (_logger.IsEnabled(LogLevel.Debug))
{
_logger.LogDebug(
"Could not send email for {Subject}. It was not handled by a notification handler and there is no SMTP configured.",
@@ -173,7 +182,6 @@ public class EmailSender : IEmailSender
while (true);
}
await _emailSenderClient.SendAsync(message);
await _emailSenderClient.SendAsync(message, expires);
}
}
@@ -3,15 +3,25 @@ using Umbraco.Cms.Core.Models.Email;
namespace Umbraco.Cms.Infrastructure.Mail.Interfaces
{
/// <summary>
/// Client for sending an email from a MimeMessage
/// Client for sending an email from a MimeMessage.
/// </summary>
public interface IEmailSenderClient
{
/// <summary>
/// Sends the email message
/// Sends the email message.
/// </summary>
/// <param name="message"></param>
/// <returns></returns>
/// <param name="message">The <see cref="EmailMessage"/> to send.</param>
[Obsolete("Please use the overload taking all parameters. Scheduled for removal in Umbraco 18.")]
public Task SendAsync(EmailMessage message);
/// <summary>
/// Sends the email message with an expiration date.
/// </summary>
/// <param name="message">The <see cref="EmailMessage"/> to send.</param>
/// <param name="expires">An optional time for expiry.</param>
public Task SendAsync(EmailMessage message, TimeSpan? expires)
#pragma warning disable CS0618 // Type or member is obsolete
=> SendAsync(message);
#pragma warning restore CS0618 // Type or member is obsolete
}
}
@@ -1,4 +1,5 @@
using System.Text;
using System.Text;
using System.Text.Json;
using Microsoft.Extensions.FileProviders;
using Microsoft.Extensions.Logging;
using Umbraco.Cms.Core.IO;
@@ -92,10 +93,15 @@ internal class PackageManifestReader : IPackageManifestReader
packageManifests.Add(packageManifest);
}
}
catch (JsonException ex)
{
throw new InvalidOperationException(
$"The package manifest file {fileInfo.PhysicalPath} could not be parsed as it does not contain valid JSON. Please see the inner exception for details.", ex);
}
catch (Exception ex)
{
_logger.LogError(ex, "Unable to load package manifest file: {FileName}", fileInfo.Name);
throw;
throw new InvalidOperationException(
$"The package manifest file {fileInfo.PhysicalPath} could not be parsed due to an unexpected error. Please see the inner exception for details.", 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()
@@ -127,5 +127,8 @@ public class UmbracoPlan : MigrationPlan
// To 16.3.0
To<V_16_3_0.AddRichTextEditorCapabilities>("{A917FCBC-C378-4A08-A36C-220C581A6581}");
To<V_16_3_0.MigrateMediaTypeLabelProperties>("{FB7073AF-DFAF-4AC1-800D-91F9BD5B5238}");
// To 16.4.0
To<V_16_4_0.CreateMissingTabs>("{6A7D3B80-8B64-4E41-A7C0-02EC39336E97}");
}
}

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