Compare commits

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

* Remove duplicate 403 ProducesResponseType attributes.

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

---------

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

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

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

* Bumped version

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

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

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

* Add tests for AllowEditInvariantFromNonDefault enabled scenario

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

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

---------

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

* clearing up

* clearing up

* Fixed style problem in safari and an unused parameter.

* Fixed lint error.

---------

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

* Hide colors

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

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

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

* Handle nullable type

* Vertically center image

* Add minimum width for SVG without dimensions

* 100% height until max height

* 100% height minus top/bottom padding

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

This reverts commit 67ada4c70f4b75dfcfa2b54ce139ec7465a17ce1.

* Revert "Handle nullable type"

This reverts commit 3130e11a4be83a18b5a7d8c1c24ee23c94d8765d.

* Removed flexbox style

* Fixed circular dependency

---------

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

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

* fix: simplify cookie explanation comment in WebProfilerRepository

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

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

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

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

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

---------

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

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

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

---------

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

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

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

---------

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

This is to support backwards-compatibility and v13 upgradability.

* Removed `uiCulture` from Vietnamese localizations

since it duplicated the English fallback texts.

* 'en' localization file formatting

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



---------

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

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

* Fix missing bracket

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

---------

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

* Redact back-office PKCE codes from the server

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

---------

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

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

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

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

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

* Clear back-office token cookies on logout

* Add configuration for backoffice cookie settings

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

* Use the "__Host-" prefix for cookie names

* docs: adds documentation on cookie settings

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

* docs: adds extra note around SameSite settings

* docs: adds extra note around SameSite settings

* Respect sites that do not use HTTPS

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

* Removed obsolete const

---------

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

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

* Fixed potential runtime errors

* Code cleanup

* Fixed Code Health Review

* Revert some changes

Commented out unused state properties and related code.

* Remove commented-out state property in block workspace view

* fix localization

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

---------

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

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

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

* Bumped version

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

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

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

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

* Add obsoletions.

* Minor spellcheck

* Ensure request cache is available before relying on it.

---------

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

* Upload BOM to Dependency Track

* Move Backoffice BOM generation to right after install

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

* Split the BOM uploads into different jobs

* Fix wrong usage of parameters

* Move order of dependency track stage

* Fix wrong umbracoVersion value

* Small fixes

* Log curl response headers

* Correct version sent to dependency track

* Adjusted curl flags

* Fix bom file path

* Fix dotnet bom file name

* Add Login UI to dependency track

* Generate BOM for E2E Tests

* Move dependency track stage

* Move acceptance test .env generation to e2e install template

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

* Use major version if public release

* Missing ')'

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

* revert wording

* move logic and update comment

---------

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

* Reload memory cache after install with package migrations.

* Improve message on install for database cache rebuild.

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

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

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

* Fix construtor breaking change

---------

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

* Reload memory cache after install with package migrations.

* Improve message on install for database cache rebuild.

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

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

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

* Fix construtor breaking change

---------

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

* ability to replace

* deprecate removeStatus

* no need to call this any longer

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

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

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

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

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

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

* docs: fix link

* Update .github/README.md

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

---------

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

* Added mandatory property to the icon picker.

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

* Extends the form control mixin to the icon picker.

* Used super.value.

* Support mandatory from settings config.

* Removed mandatoryConf.

* remove requestUpdate

---------

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

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

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

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

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

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

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

* Redone tests to fit new format

* Fix tests to use file extensions

* Adding file extensions to all other relevant tests

* Adding file filter to remaining trees

* Adding tests to ensure invalid filetypes wont show

* Encapulation and resolved minor warnings in tests.

---------

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

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

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

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

* ability to replace

* deprecate removeStatus

* no need to call this any longer

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

* Applied suggestion from code review.

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

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

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

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

* Updated tests.

* Applied suggestion from code review.

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

* Applied suggestion from code review.

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

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

* Tiptap statusbar config: enable removal of unregistered extensions

* Tiptap toolbar config: Typescript tidy-up

* Tiptap toolbar sorting amend

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

* Tiptap extension config UI amend

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

* Tiptap toolbar: adds `title` to placeholder skeleton

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

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

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

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

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

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-10-21 07:28:01 +00:00
dependabot[bot]andJacob Overgaard 5337c38f2c Bump vite from 7.1.9 to 7.1.11 in /src/Umbraco.Web.UI.Client
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 7.1.9 to 7.1.11.
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v7.1.11/packages/vite)

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

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

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

* Tweaked the published URL logic

* Code amends based on @copilot's suggestions
2025-10-20 11:51:38 +02:00
Andy Butland 5a65eb1758 Update OpenApi.json and client-side models. 2025-10-17 15:01:09 +02:00
Anders ReusandGitHub 105cb9da41 Added trashed state so when requesting content from the recycle bin via the management api it will return trashed instead of published state (#20542)
Added trashed state so when requesting content from the recycle bin via the management api, the state will be trashed instead of published.
2025-10-17 14:40:18 +02:00
Andy ButlandandJacob Overgaard a3a8be4717 Templates: Retain layout from file when loading template (closes #20524) (#20529)
Retain layout from file when loading template.
2025-10-17 10:47:21 +02:00
Jacob Overgaard d17ba805b2 build(deps): bumps @umbraco-ui/uui from 1.16.0-rc.0 to 1.16.0 2025-10-17 09:56:42 +02:00
Andy Butland 96f597e440 Merge branch 'release/16.3.1'
# Conflicts:
#	src/Umbraco.Web.UI.Client/package.json
#	version.json
2025-10-17 09:55:20 +02:00
Ben WhiteandAndy Butland 6458bb40f9 Don't use non-generic ILogger as a fallback in BlockEditorPropertyValueEditor (#20532)
Update logger service retrieval in BlockEditorPropertyValueEditor
2025-10-16 21:07:16 +02:00
Ben WhiteandGitHub 31bcbc1147 Don't use non-generic ILogger as a fallback in BlockEditorPropertyValueEditor (#20532)
Update logger service retrieval in BlockEditorPropertyValueEditor
2025-10-16 21:06:10 +02:00
Andy Butland 62edad17a1 Bumped version to 16.3.1. 2025-10-16 20:50:59 +02:00
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
Laura Neto a504fd1ef8 Bump version to 16.3.0 2025-10-16 08:22:11 +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
Andy Butland dab9df3f10 Bumped version to 16.3.0-rc4. 2025-10-08 07:59:10 +02:00
Andy Butland b036eb3a75 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:58:35 +02:00
Nikolaj Geisle 629e905187 Bump version 2025-10-06 21:21:14 +02:00
Andy ButlandandNikolaj Geisle bfd2594c7b 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.

(cherry picked from commit 184c17e2c8)
2025-10-06 21:20:56 +02:00
Andy Butland d9592aa26d Bumped version to 16.3.0-rc2. 2025-10-02 21:16:25 +02:00
Andy ButlandandKenn Jacobsen 97e0c79d94 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 21:15:21 +02:00
2373 changed files with 28111 additions and 52480 deletions
+3 -2
View File
@@ -9,7 +9,7 @@ In order to use Umbraco as a CMS and build your website with it, you should not
- Are you about to [create a pull request for Umbraco][contribution guidelines]?
- Are you trying to get to the bottom of a problem in your existing Umbraco installation?
If the answer is yes, please read on. Otherwise, make sure to head on over [to the releases page](https://releases.umbraco.com) and start using Umbraco CMS as intended.
If the answer is yes, please read on. Otherwise, make sure to head on over [to the download page](https://our.umbraco.com/download) and start using Umbraco CMS as intended.
## Table of contents
@@ -79,12 +79,13 @@ Conversely, if you are working on front-end only, you want to build the back-end
"AuthorizeCallbackLogoutPathName": "/logout",
"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 `Umbraco:Cms:Security:BackOfficeTokenCookie:SameSite` setting is set to `None`.
> If you get stuck in a login loop, try clearing your browser cookies for localhost, and make sure that the `BackOfficeTokenCookie` settings are correct. Namely, that `SameSite` should be set to `None` when running the front-end server separately.
Then run Umbraco from the command line.
+8
View File
@@ -38,6 +38,14 @@ Some important documentation links to get you started:
- [Getting to know Umbraco](https://docs.umbraco.com/umbraco-cms/fundamentals/get-to-know-umbraco)
- [Tutorials for creating a basic website and customizing the editing experience](https://docs.umbraco.com/umbraco-cms/tutorials/overview)
## Backoffice Preview
Want to see the latest backoffice UI in action? Check out our live preview:
**[backofficepreview.umbraco.com](https://backofficepreview.umbraco.com/)**
This preview is automatically deployed from the main branch and showcases the latest backoffice features and improvements. It runs from mock data and persistent edits are not supported.
## Get help
If you need a bit of feedback while building your Umbraco projects, we are [chatty on Discord](https://discord.umbraco.com). Our Discord server serves as a social space for all Umbracians. If you have any questions or need some help with a problem, head over to our [dedicated forum](https://forum.umbraco.com/) where the Umbraco Community will be happy to help.
+2 -2
View File
@@ -79,7 +79,7 @@ You can get in touch with [the core contributors team][core collabs] in multiple
- If there's an existing issue on the issue tracker then that's a good place to leave questions and discuss how to start or move forward.
- If you want to ask questions on some code you've already written you can create a draft pull request, [detailed in a GitHub blog post][draft prs].
- Unsure where to start? Did something not work as expected? Try leaving a note in the [forum][forum]. The team monitors that one closely, so one of us will be on hand and ready to point you in the right direction.
- Unsure where to start? Did something not work as expected? Try leaving a note in the ["Contributing to Umbraco"][contrib forum] forum. The team monitors that one closely, so one of us will be on hand and ready to point you in the right direction.
<!-- Local -->
@@ -90,7 +90,7 @@ You can get in touch with [the core contributors team][core collabs] in multiple
[sync fork ext]: http://robots.thoughtbot.com/post/5133345960/keeping-a-git-fork-updated "Details on keeping a git fork updated"
[draft prs]: https://github.blog/2019-02-14-introducing-draft-pull-requests/ "Github's blog post providing details on draft pull requests"
[forum]: https://forum.umbraco.com/
[contrib forum]: https://our.umbraco.com/forum/contributing-to-umbraco-cms/
[Umbraco CMS repo]: https://github.com/umbraco/Umbraco-CMS
[up for grabs issues]: https://github.com/umbraco/Umbraco-CMS/issues?q=is%3Aissue+is%3Aopen+label%3Acommunity%2Fup-for-grabs
[issue tracker]: https://github.com/umbraco/Umbraco-CMS/issues
+17 -7
View File
@@ -31,8 +31,6 @@ Bootstrap, build, and test the repository:
- `cd src/Umbraco.Web.UI`
- `dotnet run --no-build` -- Application runs on https://localhost:44339 and http://localhost:11000
Check out [BUILD.md](./BUILD.md) for more detailed instructions.
## Validation
- ALWAYS run through at least one complete end-to-end scenario after making changes.
@@ -96,22 +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",
"BackOfficeTokenCookie": {
"Enabled": true,
"SameSite": "None"
}
```
2. Run backend: `cd src/Umbraco.Web.UI && dotnet run --no-build`
3. Run frontend dev server: `cd src/Umbraco.Web.UI.Client && npm run dev:server`
4. Access backoffice: `http://localhost:5173/` (no `/umbraco` prefix)
5. Changes to TypeScript/Lit files hot reload automatically
**Important:** Remove the `BackOfficeHost` configuration before committing or switching back to production mode.
### Backend-Only Development
For backend-only changes, disable frontend builds:
@@ -52,7 +52,7 @@ jobs:
for (const item of items) {
const releaseLabels = (item.labels || [])
.map(l => (typeof l === "string" ? l : l.name)) // always get the name
.filter(n => typeof n === "string" && n.startsWith("release/"));
.filter(n => typeof n === "string" && n.startsWith("release/") && n !== "release/no-notes");
if (releaseLabels.length === 0) continue;
core.info(`#${item.number}: ${releaseLabels.join(", ")}`);
+2
View File
@@ -101,11 +101,13 @@
"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__KEEPUSERLOGGEDIN": "true",
"UMBRACO__CMS__SECURITY__BACKOFFICETOKENCOOKIE__ENABLED": "true",
"UMBRACO__CMS__SECURITY__BACKOFFICETOKENCOOKIE__SAMESITE": "None"
},
"sourceFileMap": {
-3
View File
@@ -1,8 +1,5 @@
{
"cSpell.words": [
"backoffice",
"pickable",
"Pickable",
"unprovide",
"Unproviding"
],
+2 -2
View File
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<Project>
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<TargetFramework>net9.0</TargetFramework>
<Company>Umbraco HQ</Company>
<Authors>Umbraco</Authors>
<Copyright>Copyright © Umbraco $([System.DateTime]::Today.ToString('yyyy'))</Copyright>
@@ -40,7 +40,7 @@
<!-- Package Validation -->
<PropertyGroup>
<GenerateCompatibilitySuppressionFile>false</GenerateCompatibilitySuppressionFile>
<EnablePackageValidation>false</EnablePackageValidation> <!-- TODO (V17): Set to true with version 17.0.0 once this version is released. -->
<EnablePackageValidation>true</EnablePackageValidation>
<PackageValidationBaselineVersion>16.0.0</PackageValidationBaselineVersion>
<EnableStrictModeForCompatibleFrameworksInPackage>true</EnableStrictModeForCompatibleFrameworksInPackage>
<EnableStrictModeForCompatibleTfms>true</EnableStrictModeForCompatibleTfms>
+58 -37
View File
@@ -5,35 +5,36 @@
</PropertyGroup>
<!-- Global packages (private, build-time packages for all projects) -->
<ItemGroup>
<GlobalPackageReference Include="Nerdbank.GitVersioning" Version="3.8.118" />
<GlobalPackageReference Include="Nerdbank.GitVersioning" Version="3.7.115" />
<GlobalPackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.556" />
<GlobalPackageReference Include="Umbraco.Code" Version="2.4.0" />
<GlobalPackageReference Include="Umbraco.Code" Version="2.3.0" />
<GlobalPackageReference Include="Umbraco.GitVersioning.Extensions" Version="0.2.0" />
</ItemGroup>
<!-- Microsoft packages -->
<ItemGroup>
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation" Version="10.0.0" />
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="4.14.0" />
<PackageVersion Include="Microsoft.Data.Sqlite" Version="10.0.0" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="10.0.0" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.SqlServer" Version="10.0.0" />
<PackageVersion Include="Microsoft.Extensions.Caching.Abstractions" Version="10.0.0" />
<PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="10.0.0" />
<PackageVersion Include="Microsoft.Extensions.Configuration.Abstractions" Version="10.0.0" />
<PackageVersion Include="Microsoft.Extensions.Configuration.Json" Version="10.0.0" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="10.0.0" />
<PackageVersion Include="Microsoft.Extensions.FileProviders.Embedded" Version="10.0.0" />
<PackageVersion Include="Microsoft.Extensions.FileProviders.Physical" Version="10.0.0" />
<PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="10.0.0" />
<PackageVersion Include="Microsoft.Extensions.Http" Version="10.0.0" />
<PackageVersion Include="Microsoft.Extensions.Identity.Core" Version="10.0.0" />
<PackageVersion Include="Microsoft.Extensions.Identity.Stores" Version="10.0.0" />
<PackageVersion Include="Microsoft.Extensions.Logging" Version="10.0.0" />
<PackageVersion Include="Microsoft.Extensions.Options" Version="10.0.0" />
<PackageVersion Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="10.0.0" />
<PackageVersion Include="Microsoft.Extensions.Options.DataAnnotations" Version="10.0.0" />
<PackageVersion Include="Microsoft.Extensions.Caching.Hybrid" Version="10.0.0" />
<PackageVersion Include="System.Linq.Async" Version="7.0.0" />
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation" Version="9.0.4" />
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="4.13.0" />
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp.Workspaces" 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.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" />
<PackageVersion Include="Microsoft.Extensions.Configuration.Json" Version="9.0.4" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="9.0.4" />
<PackageVersion Include="Microsoft.Extensions.FileProviders.Embedded" Version="9.0.4" />
<PackageVersion Include="Microsoft.Extensions.FileProviders.Physical" Version="9.0.4" />
<PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="9.0.4" />
<PackageVersion Include="Microsoft.Extensions.Http" Version="9.0.4" />
<PackageVersion Include="Microsoft.Extensions.Identity.Core" Version="9.0.4" />
<PackageVersion Include="Microsoft.Extensions.Identity.Stores" Version="9.0.4" />
<PackageVersion Include="Microsoft.Extensions.Logging" Version="9.0.4" />
<PackageVersion Include="Microsoft.Extensions.Options" Version="9.0.4" />
<PackageVersion Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="9.0.4" />
<PackageVersion Include="Microsoft.Extensions.Options.DataAnnotations" Version="9.0.4" />
<PackageVersion Include="Microsoft.Extensions.Caching.Hybrid" Version="9.4.0" />
</ItemGroup>
<!-- Umbraco packages -->
<ItemGroup>
@@ -46,21 +47,21 @@
<PackageVersion Include="Dazinator.Extensions.FileProviders" Version="2.0.0" />
<PackageVersion Include="Examine" Version="3.7.1" />
<PackageVersion Include="Examine.Core" Version="3.7.1" />
<PackageVersion Include="HtmlAgilityPack" Version="1.12.4" />
<PackageVersion Include="HtmlAgilityPack" Version="1.12.1" />
<PackageVersion Include="JsonPatch.Net" Version="3.3.0" />
<PackageVersion Include="K4os.Compression.LZ4" Version="1.3.8" />
<PackageVersion Include="MailKit" Version="4.14.0" />
<PackageVersion Include="MailKit" Version="4.11.0" />
<PackageVersion Include="Markdown" Version="2.2.1" />
<PackageVersion Include="MessagePack" Version="3.1.4" />
<PackageVersion Include="MessagePack" Version="3.1.3" />
<PackageVersion Include="MiniProfiler.AspNetCore.Mvc" Version="4.5.4" />
<PackageVersion Include="MiniProfiler.Shared" Version="4.5.4" />
<PackageVersion Include="ncrontab" Version="3.4.0" />
<PackageVersion Include="NPoco" Version="6.1.0" />
<PackageVersion Include="NPoco.SqlServer" Version="6.1.0" />
<PackageVersion Include="OpenIddict.Abstractions" Version="7.1.0" />
<PackageVersion Include="OpenIddict.AspNetCore" Version="7.1.0" />
<PackageVersion Include="OpenIddict.EntityFrameworkCore" Version="7.1.0" />
<PackageVersion Include="Serilog" Version="4.3.0" />
<PackageVersion Include="ncrontab" Version="3.3.3" />
<PackageVersion Include="NPoco" Version="5.7.1" />
<PackageVersion Include="NPoco.SqlServer" Version="5.7.1" />
<PackageVersion Include="OpenIddict.Abstractions" Version="6.2.1" />
<PackageVersion Include="OpenIddict.AspNetCore" Version="6.2.1" />
<PackageVersion Include="OpenIddict.EntityFrameworkCore" Version="6.2.1" />
<PackageVersion Include="Serilog" Version="4.2.0" />
<PackageVersion Include="Serilog.AspNetCore" Version="9.0.0" />
<PackageVersion Include="Serilog.Enrichers.Process" Version="3.0.0" />
<PackageVersion Include="Serilog.Enrichers.Thread" Version="4.0.0" />
@@ -70,13 +71,33 @@
<PackageVersion Include="Serilog.Formatting.Compact.Reader" Version="4.0.0" />
<PackageVersion Include="Serilog.Settings.Configuration" Version="9.0.0" />
<PackageVersion Include="Serilog.Sinks.Async" Version="2.1.0" />
<PackageVersion Include="Serilog.Sinks.File" Version="7.0.0" />
<PackageVersion Include="Serilog.Sinks.File" Version="6.0.0" />
<PackageVersion Include="Serilog.Sinks.Map" Version="2.0.0" />
<PackageVersion Include="SixLabors.ImageSharp" Version="3.1.11" />
<PackageVersion Include="SixLabors.ImageSharp.Web" Version="3.2.0" />
<PackageVersion Include="Swashbuckle.AspNetCore" Version="10.0.1" />
<PackageVersion Include="SixLabors.ImageSharp.Web" Version="3.1.5" />
<PackageVersion Include="Swashbuckle.AspNetCore" Version="8.1.1" />
</ItemGroup>
<!-- Transitive pinned versions (only required because our direct dependencies have vulnerable versions of transitive dependencies) -->
<ItemGroup>
<!-- Microsoft.EntityFrameworkCore.SqlServer and NPoco.SqlServer brings in a vulnerable version of Azure.Identity -->
<!-- Take top-level depedendency on Azure.Identity, because Microsoft.EntityFrameworkCore.SqlServer depends on a vulnerable version -->
<PackageVersion Include="Azure.Identity" Version="1.13.2" />
<!-- Microsoft.EntityFrameworkCore.SqlServer brings in a vulnerable version of System.Runtime.Caching -->
<PackageVersion Include="System.Runtime.Caching" Version="9.0.4" />
<!-- Dazinator.Extensions.FileProviders brings in a vulnerable version of System.Net.Http -->
<PackageVersion Include="System.Net.Http" Version="4.3.4" />
<!-- Examine brings in a vulnerable version of System.Security.Cryptography.Xml -->
<PackageVersion Include="System.Security.Cryptography.Xml" Version="9.0.4" />
<!-- Dazinator.Extensions.FileProviders and MiniProfiler.AspNetCore.Mvc brings in a vulnerable version of System.Text.RegularExpressions -->
<PackageVersion Include="System.Text.RegularExpressions" Version="4.3.1" />
<!-- OpenIddict.AspNetCore, Npoco.SqlServer and Microsoft.EntityFrameworkCore.SqlServer brings in a vulnerable version of Microsoft.IdentityModel.JsonWebTokens -->
<!-- Take top-level depedendency on Microsoft.IdentityModel.JsonWebTokens, because OpenIddict.AspNetCore, Npoco.SqlServer and Microsoft.EntityFrameworkCore.SqlServer depends on a vulnerable version -->
<PackageVersion Include="Microsoft.IdentityModel.JsonWebTokens" Version="8.8.0" />
<!-- Azure.Identity, Microsoft.EntityFrameworkCore.SqlServer and Dazinator.Extensions.FileProviders brings in a legacy version of System.Text.Encodings.Web -->
<PackageVersion Include="System.Text.Encodings.Web" Version="9.0.4" />
<!-- NPoco.SqlServer brings in a vulnerable version of Microsoft.Data.SqlClient -->
<PackageVersion Include="Microsoft.Data.SqlClient" Version="6.0.1" />
<!-- Examine.Lucene brings in a vulnerable version of Lucene.Net.Replicator -->
<PackageVersion Include="Lucene.Net.Replicator" Version="4.8.0-beta00017" />
</ItemGroup>
</Project>
+8 -8
View File
@@ -365,8 +365,8 @@ stages:
testFilter: "(FullyQualifiedName~Umbraco.Infrastructure.Service)"
LinuxPart3Of3:
vmImage: "ubuntu-latest"
# Filter tests that are not part of the Umbraco.Infrastructure and ManagementApi namespace. So this will run all tests that are not part of the Umbraco.Infrastructure and ManagementApi namespace
testFilter: "(FullyQualifiedName!~Umbraco.Infrastructure) & (FullyQualifiedName!~ManagementApi)"
# Filter tests that are not part of the Umbraco.Infrastructure namespace. So this will run all tests that are not part of the Umbraco.Infrastructure namespace
testFilter: "(FullyQualifiedName!~Umbraco.Infrastructure)"
macOSPart1Of3:
vmImage: "macOS-latest"
# Filter tests that are part of the Umbraco.Infrastructure namespace but not part of the Umbraco.Infrastructure.Service namespace
@@ -377,8 +377,8 @@ stages:
testFilter: "(FullyQualifiedName~Umbraco.Infrastructure.Service)"
macOSPart3Of3:
vmImage: "macOS-latest"
# Filter tests that are not part of the Umbraco.Infrastructure and ManagementApi namespace. So this will run all tests that are not part of the Umbraco.Infrastructure and the ManagementApi namespace
testFilter: "(FullyQualifiedName!~Umbraco.Infrastructure) & (FullyQualifiedName!~ManagementApi)"
# Filter tests that are not part of the Umbraco.Infrastructure namespace.
testFilter: "(FullyQualifiedName!~Umbraco.Infrastructure)"
pool:
vmImage: $(vmImage)
variables:
@@ -441,8 +441,8 @@ stages:
vmImage: "windows-latest"
Tests__Database__DatabaseType: LocalDb
Tests__Database__SQLServerMasterConnectionString: N/A
# Filter tests that are not part of the Umbraco.Infrastructure and ManagementApi namespace. So this will run all tests that are not part of the Umbraco.Infrastructure and ManagementApi namespace
testFilter: "(FullyQualifiedName!~Umbraco.Infrastructure) & (FullyQualifiedName!~ManagementApi)"
# Filter tests that are not part of the Umbraco.Infrastructure namespace. So this will run all tests that are not part of the Umbraco.Infrastructure namespace
testFilter: "(FullyQualifiedName!~Umbraco.Infrastructure)"
LinuxPart1Of3:
vmImage: "ubuntu-latest"
SA_PASSWORD: UmbracoIntegration123!
@@ -462,8 +462,8 @@ stages:
SA_PASSWORD: UmbracoIntegration123!
Tests__Database__DatabaseType: SqlServer
Tests__Database__SQLServerMasterConnectionString: "Server=(local);User Id=sa;Password=$(SA_PASSWORD);TrustServerCertificate=True"
# Filter tests that are not part of the Umbraco.Infrastructure and ManagementApi namespace. So this will run all tests that are not part of the Umbraco.Infrastructure and ManagementApi namespace
testFilter: "(FullyQualifiedName!~Umbraco.Infrastructure) & (FullyQualifiedName!~ManagementApi)"
# Filter tests that are not part of the Umbraco.Infrastructure namespace. So this will run all tests that are not part of the Umbraco.Infrastructure namespace
testFilter: "(FullyQualifiedName!~Umbraco.Infrastructure)"
pool:
vmImage: $(vmImage)
steps:
+32 -52
View File
@@ -4,13 +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
- v17/dev
- v16/dev
parameters:
- name: skipIntegrationTests
@@ -21,7 +19,7 @@ parameters:
- name: differentAppSettingsAcceptanceTests
displayName: Run acceptance tests with different app settings
type: boolean
default: true
default: false
- name: skipDefaultConfigAcceptanceTests
displayName: Skip tests with DefaultConfig
@@ -123,39 +121,31 @@ stages:
matrix:
# Windows:
# vmImage: 'windows-latest'
# We split the tests into 4 parts for each OS to reduce the time it takes to run them on the pipeline
LinuxPart1Of4:
# We split the tests into 3 parts for each OS to reduce the time it takes to run them on the pipeline
LinuxPart1Of3:
vmImage: "ubuntu-latest"
# Filter tests that are part of the Umbraco.Infrastructure namespace but not part of the Umbraco.Infrastructure.Service namespace
testFilter: "(FullyQualifiedName~Umbraco.Infrastructure) & (FullyQualifiedName!~Umbraco.Infrastructure.Service)"
LinuxPart2Of4:
LinuxPart2Of3:
vmImage: "ubuntu-latest"
# Filter tests that are part of the Umbraco.Infrastructure.Service namespace
testFilter: "(FullyQualifiedName~Umbraco.Infrastructure.Service)"
LinuxPart3Of4:
LinuxPart3Of3:
vmImage: "ubuntu-latest"
# Filter tests that are not part of the Umbraco.Infrastructure and ManagementApi namespace. So this will run all tests that are not part of the Umbraco.Infrastructure namespace
testFilter: "(FullyQualifiedName!~Umbraco.Infrastructure) & (FullyQualifiedName!~ManagementApi)"
LinuxPart4Of4:
vmImage: "ubuntu-latest"
# Filter tests that are part of the ManagementApi namespace
testFilter: "(FullyQualifiedName~ManagementApi)"
macOSPart1Of4:
# Filter tests that are not part of the Umbraco.Infrastructure namespace. So this will run all tests that are not part of the Umbraco.Infrastructure namespace
testFilter: "(FullyQualifiedName!~Umbraco.Infrastructure)"
macOSPart1Of3:
vmImage: "macOS-latest"
# Filter tests that are part of the Umbraco.Infrastructure namespace but not part of the Umbraco.Infrastructure.Service namespace
testFilter: "(FullyQualifiedName~Umbraco.Infrastructure) & (FullyQualifiedName!~Umbraco.Infrastructure.Service)"
macOSPart2Of4:
macOSPart2Of3:
vmImage: "macOS-latest"
# Filter tests that are part of the Umbraco.Infrastructure.Service namespace
testFilter: "(FullyQualifiedName~Umbraco.Infrastructure.Service)"
macOSPart3Of4:
macOSPart3Of3:
vmImage: "macOS-latest"
# Filter tests that are not part of the Umbraco.Infrastructure and ManagementApi namespace.
testFilter: "(FullyQualifiedName!~Umbraco.Infrastructure) & (FullyQualifiedName!~ManagementApi)"
macOSPart4Of4:
vmImage: "macOS-latest"
# Filter tests that are part of the ManagementApi namespace.
testFilter: "(FullyQualifiedName~ManagementApi)"
# Filter tests that are not part of the Umbraco.Infrastructure namespace.
testFilter: "(FullyQualifiedName!~Umbraco.Infrastructure)"
pool:
vmImage: $(vmImage)
variables:
@@ -195,55 +185,43 @@ stages:
SA_PASSWORD: UmbracoAcceptance123!
strategy:
matrix:
# We split the tests into 4 parts for each OS to reduce the time it takes to run them on the pipeline
WindowsPart1Of4:
# We split the tests into 3 parts for each OS to reduce the time it takes to run them on the pipeline
WindowsPart1Of3:
vmImage: "windows-latest"
Tests__Database__DatabaseType: LocalDb
Tests__Database__SQLServerMasterConnectionString: N/A
# Filter tests that are part of the Umbraco.Infrastructure namespace but not part of the Umbraco.Infrastructure.Service namespace
testFilter: "(FullyQualifiedName~Umbraco.Infrastructure) & (FullyQualifiedName!~Umbraco.Infrastructure.Service)"
WindowsPart2Of4:
WindowsPart2Of3:
vmImage: "windows-latest"
Tests__Database__DatabaseType: LocalDb
Tests__Database__SQLServerMasterConnectionString: N/A
# Filter tests that are part of the Umbraco.Infrastructure.Service namespace
testFilter: "(FullyQualifiedName~Umbraco.Infrastructure.Service)"
WindowsPart3Of4:
WindowsPart3Of3:
vmImage: "windows-latest"
Tests__Database__DatabaseType: LocalDb
Tests__Database__SQLServerMasterConnectionString: N/A
# Filter tests that are not part of the Umbraco.Infrastructure and ManagementApi namespace.
testFilter: "(FullyQualifiedName!~Umbraco.Infrastructure) & (FullyQualifiedName!~ManagementApi)"
WindowsPart4Of4:
vmImage: "windows-latest"
Tests__Database__DatabaseType: LocalDb
Tests__Database__SQLServerMasterConnectionString: N/A
# Filter tests that are part of the ManagementApi namespace.
testFilter: "(FullyQualifiedName~ManagementApi)"
LinuxPart1Of4:
# Filter tests that are not part of the Umbraco.Infrastructure namespace. So this will run all tests that are not part of the Umbraco.Infrastructure namespace
testFilter: "(FullyQualifiedName!~Umbraco.Infrastructure)"
LinuxPart1Of3:
vmImage: "ubuntu-latest"
Tests__Database__DatabaseType: SqlServer
Tests__Database__SQLServerMasterConnectionString: "Server=(local);User Id=sa;Password=$(SA_PASSWORD);Encrypt=True;TrustServerCertificate=True"
# Filter tests that are part of the Umbraco.Infrastructure namespace but not part of the Umbraco.Infrastructure.Service namespace
testFilter: "(FullyQualifiedName~Umbraco.Infrastructure) & (FullyQualifiedName!~Umbraco.Infrastructure.Service)"
LinuxPart2Of4:
LinuxPart2Of3:
vmImage: "ubuntu-latest"
Tests__Database__DatabaseType: SqlServer
Tests__Database__SQLServerMasterConnectionString: "Server=(local);User Id=sa;Password=$(SA_PASSWORD);Encrypt=True;TrustServerCertificate=True"
# Filter tests that are part of the Umbraco.Infrastructure.Service namespace
testFilter: "(FullyQualifiedName~Umbraco.Infrastructure.Service)"
LinuxPart3Of4:
LinuxPart3Of3:
vmImage: "ubuntu-latest"
Tests__Database__DatabaseType: SqlServer
Tests__Database__SQLServerMasterConnectionString: "Server=(local);User Id=sa;Password=$(SA_PASSWORD);Encrypt=True;TrustServerCertificate=True"
# Filter tests that are not part of the Umbraco.Infrastructure and ManagementApi namespace.
testFilter: "(FullyQualifiedName!~Umbraco.Infrastructure) & (FullyQualifiedName!~ManagementApi)"
LinuxPart4Of4:
vmImage: "ubuntu-latest"
Tests__Database__DatabaseType: SqlServer
Tests__Database__SQLServerMasterConnectionString: "Server=(local);User Id=sa;Password=$(SA_PASSWORD);Encrypt=True;TrustServerCertificate=True"
# Filter tests that are part of the ManagementApi namespace.
testFilter: "(FullyQualifiedName~ManagementApi)"
# Filter tests that are not part of the Umbraco.Infrastructure namespace. So this will run all tests that are not part of the Umbraco.Infrastructure namespace
testFilter: "(FullyQualifiedName!~Umbraco.Infrastructure)"
pool:
vmImage: $(vmImage)
steps:
@@ -315,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
@@ -496,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
@@ -505,7 +485,7 @@ stages:
jobs:
- job:
displayName: E2E Tests with Different App settings (SQL Server)
condition: ${{ eq(parameters.differentAppSettingsAcceptanceTests, true) }}
condition: ${{ or(eq(parameters.differentAppSettingsAcceptanceTests, true), eq(parameters.skipDefaultConfigAcceptanceTests, true)) }}
timeoutInMinutes: 180
variables:
SA_PASSWORD: UmbracoAcceptance123!
@@ -695,4 +675,4 @@ stages:
--data "$PAYLOAD" \
"$SLACK_WEBHOOK_URL"
env:
SLACK_WEBHOOK_URL: $(E2ESLACKWEBHOOKURL)
SLACK_WEBHOOK_URL: $(E2ESLACKWEBHOOKURL)
+1 -1
View File
@@ -1,6 +1,6 @@
{
"sdk": {
"version": "10.0.100",
"version": "9.0.306",
"rollForward": "latestFeature",
"allowPrerelease": false
}
@@ -4,7 +4,7 @@ using Microsoft.AspNetCore.Mvc.ApiExplorer;
using Microsoft.AspNetCore.Mvc.Controllers;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using Microsoft.OpenApi;
using Microsoft.OpenApi.Models;
using Swashbuckle.AspNetCore.SwaggerGen;
using Umbraco.Cms.Api.Common.OpenApi;
using Umbraco.Extensions;
@@ -21,13 +21,9 @@ internal sealed class HideBackOfficeTokensHandler
INotificationHandler<UserLogoutSuccessNotification>
{
private const string RedactedTokenValue = "[redacted]";
// The __Host- prefix enforces secure cookies at browser level (requires Secure, Path=/, no Domain).
// For local development over HTTP, we use a simpler prefix to avoid browser rejection.
private const string SecureCookiePrefix = "__Host-";
private const string AccessTokenCookieName = "umbAccessToken";
private const string RefreshTokenCookieName = "umbRefreshToken";
private const string PkceCodeCookieName = "umbPkceCode";
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;
@@ -63,13 +59,13 @@ internal sealed class HideBackOfficeTokensHandler
if (context.Response.AccessToken is not null)
{
SetCookie(httpContext, AccessTokenCookieName, context.Response.AccessToken);
SetCookie(httpContext, AccessTokenCookieKey, context.Response.AccessToken);
context.Response.AccessToken = RedactedTokenValue;
}
if (context.Response.RefreshToken is not null)
{
SetCookie(httpContext, RefreshTokenCookieName, context.Response.RefreshToken);
SetCookie(httpContext, RefreshTokenCookieKey, context.Response.RefreshToken);
context.Response.RefreshToken = RedactedTokenValue;
}
@@ -91,7 +87,7 @@ internal sealed class HideBackOfficeTokensHandler
if (context.Response.Code is not null)
{
SetCookie(GetHttpContext(), PkceCodeCookieName, context.Response.Code);
SetCookie(GetHttpContext(), PkceCodeCookieKey, context.Response.Code);
context.Response.Code = RedactedTokenValue;
}
@@ -109,16 +105,14 @@ internal sealed class HideBackOfficeTokensHandler
return ValueTask.CompletedTask;
}
HttpContext httpContext = GetHttpContext();
// Handle when the PKCE code is being exchanged for an access token.
if (context.Request.Code == RedactedTokenValue
&& TryGetCookie(httpContext, PkceCodeCookieName, out var code))
&& TryGetCookie(PkceCodeCookieKey, out var code))
{
context.Request.Code = code;
// We won't need the PKCE cookie after this, let's remove it.
RemoveCookie(httpContext, PkceCodeCookieName);
RemoveCookie(GetHttpContext(), PkceCodeCookieKey);
}
else
{
@@ -129,7 +123,7 @@ internal sealed class HideBackOfficeTokensHandler
// Handle when a refresh token is being exchanged for a new access token.
if (context.Request.RefreshToken == RedactedTokenValue
&& TryGetCookie(httpContext, RefreshTokenCookieName, out var refreshToken))
&& TryGetCookie(RefreshTokenCookieKey, out var refreshToken))
{
context.Request.RefreshToken = refreshToken;
}
@@ -155,7 +149,7 @@ internal sealed class HideBackOfficeTokensHandler
return ValueTask.CompletedTask;
}
if (TryGetCookie(GetHttpContext(), AccessTokenCookieName, out var accessToken))
if (TryGetCookie(AccessTokenCookieKey, out var accessToken))
{
context.AccessToken = accessToken;
}
@@ -165,8 +159,8 @@ internal sealed class HideBackOfficeTokensHandler
public void Handle(UserLogoutSuccessNotification notification)
{
HttpContext? httpContext = _httpContextAccessor.HttpContext;
if (httpContext is null)
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,
@@ -174,32 +168,23 @@ internal sealed class HideBackOfficeTokensHandler
return;
}
RemoveCookie(httpContext, AccessTokenCookieName);
RemoveCookie(httpContext, RefreshTokenCookieName);
context.Response.Cookies.Delete(AccessTokenCookieKey);
context.Response.Cookies.Delete(RefreshTokenCookieKey);
}
private HttpContext GetHttpContext()
=> _httpContextAccessor.GetRequiredHttpContext();
private string GetCookieKey(HttpContext httpContext, string cookieName)
=> _globalSettings.UseHttps || httpContext.Request.IsHttps
? $"{SecureCookiePrefix}{cookieName}"
: cookieName;
private void SetCookie(HttpContext httpContext, string cookieName, string value)
private void SetCookie(HttpContext httpContext, string key, string value)
{
var key = GetCookieKey(httpContext, cookieName);
var cookieValue = EncryptionHelper.Encrypt(value, _dataProtectionProvider);
RemoveCookie(httpContext, cookieName);
RemoveCookie(httpContext, key);
httpContext.Response.Cookies.Append(key, cookieValue, GetCookieOptions(httpContext));
}
private void RemoveCookie(HttpContext httpContext, string cookieName)
{
var key = GetCookieKey(httpContext, cookieName);
httpContext.Response.Cookies.Delete(key, GetCookieOptions(httpContext));
}
private void RemoveCookie(HttpContext httpContext, string key)
=> httpContext.Response.Cookies.Delete(key, GetCookieOptions(httpContext));
private CookieOptions GetCookieOptions(HttpContext httpContext) =>
new()
@@ -226,10 +211,9 @@ internal sealed class HideBackOfficeTokensHandler
SameSite = ParseSameSiteMode(_backOfficeTokenCookieSettings.SameSite),
};
private bool TryGetCookie(HttpContext httpContext, string cookieName, [NotNullWhen(true)] out string? value)
private bool TryGetCookie(string key, [NotNullWhen(true)] out string? value)
{
var key = GetCookieKey(httpContext, cookieName);
if (httpContext.Request.Cookies.TryGetValue(key, out var cookieValue))
if (GetHttpContext().Request.Cookies.TryGetValue(key, out var cookieValue))
{
value = EncryptionHelper.Decrypt(cookieValue, _dataProtectionProvider);
return true;
@@ -9,9 +9,9 @@ using Umbraco.Cms.Api.Common.Security;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Configuration.Models;
using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Infrastructure.BackgroundJobs;
using Umbraco.Cms.Infrastructure.BackgroundJobs.Jobs.DistributedJobs;
using Umbraco.Cms.Core.Notifications;
using Umbraco.Cms.Infrastructure.BackgroundJobs.Jobs;
using Umbraco.Extensions;
namespace Umbraco.Cms.Api.Common.DependencyInjection;
@@ -29,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 =>
@@ -115,24 +120,27 @@ public static class UmbracoBuilderAuthExtensions
configuration.UseSingletonHandler<ProcessRequestContextHandler>().SetOrder(OpenIddict.Server.AspNetCore.OpenIddictServerAspNetCoreHandlers.ResolveRequestUri.Descriptor.Order - 1);
});
options.AddEventHandler<OpenIddictServerEvents.ApplyTokenResponseContext>(configuration =>
if (hideBackOfficeTokens)
{
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);
});
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.
@@ -158,18 +166,24 @@ public static class UmbracoBuilderAuthExtensions
configuration.UseSingletonHandler<ProcessRequestContextHandler>().SetOrder(OpenIddict.Validation.AspNetCore.OpenIddictValidationAspNetCoreHandlers.ResolveRequestUri.Descriptor.Order - 1);
});
options.AddEventHandler<OpenIddictValidationEvents.ProcessAuthenticationContext>(configuration =>
if (hideBackOfficeTokens)
{
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);
});
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.AddSingleton<IDistributedBackgroundJob, OpenIddictCleanupJob>();
builder.Services.AddRecurringBackgroundJob<OpenIddictCleanupJob>();
builder.Services.ConfigureOptions<ConfigureOpenIddict>();
builder.AddNotificationHandler<UserLogoutSuccessNotification, HideBackOfficeTokensHandler>();
if (hideBackOfficeTokens)
{
builder.AddNotificationHandler<UserLogoutSuccessNotification, HideBackOfficeTokensHandler>();
}
}
}
@@ -8,11 +8,11 @@ namespace Umbraco.Extensions;
public static class MethodInfoApiCommonExtensions
{
public static string GetMapToApiVersionAttributeValue(this MethodInfo methodInfo)
public static string? GetMapToApiVersionAttributeValue(this MethodInfo methodInfo)
{
MapToApiVersionAttribute[] mapToApis = methodInfo.GetCustomAttributes(typeof(MapToApiVersionAttribute), inherit: true).Cast<MapToApiVersionAttribute>().ToArray();
return string.Join("|", mapToApis.SelectMany(x => x.Versions));
return string.Join("|", mapToApis.SelectMany(x=>x.Versions));
}
public static string? GetMapToApiAttributeValue(this MethodInfo methodInfo)
@@ -1,27 +1,25 @@
using System.Reflection;
using System.Runtime.Serialization;
using System.Text.Json.Nodes;
using Microsoft.OpenApi;
using Microsoft.OpenApi.Any;
using Microsoft.OpenApi.Models;
using Swashbuckle.AspNetCore.SwaggerGen;
namespace Umbraco.Cms.Api.Common.OpenApi;
public class EnumSchemaFilter : ISchemaFilter
{
public void Apply(IOpenApiSchema model, SchemaFilterContext context)
public void Apply(OpenApiSchema model, SchemaFilterContext context)
{
if (model is not OpenApiSchema schema || context.Type.IsEnum is false)
if (context.Type.IsEnum)
{
return;
}
schema.Type = JsonSchemaType.String;
schema.Format = null;
schema.Enum = new List<JsonNode>();
foreach (var name in Enum.GetNames(context.Type))
{
var actualName = context.Type.GetField(name)?.GetCustomAttribute<EnumMemberAttribute>()?.Value ?? name;
schema.Enum.Add(actualName);
model.Type = "string";
model.Format = null;
model.Enum.Clear();
foreach (var name in Enum.GetNames(context.Type))
{
var actualName = context.Type.GetField(name)?.GetCustomAttribute<EnumMemberAttribute>()?.Value ?? name;
model.Enum.Add(new OpenApiString(actualName));
}
}
}
}
@@ -1,4 +1,4 @@
using Microsoft.OpenApi;
using Microsoft.OpenApi.Models;
using Swashbuckle.AspNetCore.SwaggerGen;
using Umbraco.Extensions;
@@ -21,32 +21,25 @@ public class MimeTypeDocumentFilter : IDocumentFilter
}
OpenApiOperation[] operations = swaggerDoc.Paths
.SelectMany(path => path.Value.Operations?.Values ?? Enumerable.Empty<OpenApiOperation>())
.SelectMany(path => path.Value.Operations.Values)
.ToArray();
void RemoveUnwantedMimeTypes(IDictionary<string, OpenApiMediaType>? content)
void RemoveUnwantedMimeTypes(IDictionary<string, OpenApiMediaType> content)
{
if (content is null || content.ContainsKey("application/json") is false)
if (content.ContainsKey("application/json"))
{
return;
content.RemoveAll(r => r.Key != "application/json");
}
content.RemoveAll(r => r.Key != "application/json");
}
OpenApiRequestBody[] requestBodies = operations
.Select(operation => operation.RequestBody)
.OfType<OpenApiRequestBody>()
.ToArray();
OpenApiRequestBody[] requestBodies = operations.Select(operation => operation.RequestBody).WhereNotNull().ToArray();
foreach (OpenApiRequestBody requestBody in requestBodies)
{
RemoveUnwantedMimeTypes(requestBody.Content);
}
OpenApiResponse[] responses = operations
.SelectMany(operation => operation.Responses?.Values ?? Enumerable.Empty<IOpenApiResponse>())
.OfType<OpenApiResponse>()
.ToArray();
OpenApiResponse[] responses = operations.SelectMany(operation => operation.Responses.Values).WhereNotNull().ToArray();
foreach (OpenApiResponse response in responses)
{
RemoveUnwantedMimeTypes(response.Content);
@@ -1,4 +1,4 @@
using Microsoft.OpenApi;
using Microsoft.OpenApi.Models;
using Swashbuckle.AspNetCore.SwaggerGen;
namespace Umbraco.Cms.Api.Common.OpenApi;
@@ -20,6 +20,6 @@ public class RemoveSecuritySchemesDocumentFilter : IDocumentFilter
return;
}
swaggerDoc.Components?.SecuritySchemes?.Clear();
swaggerDoc.Components.SecuritySchemes.Clear();
}
}
@@ -15,6 +15,20 @@ public class SubTypesSelector : ISubTypesSelector
private readonly IEnumerable<ISubTypesHandler> _subTypeHandlers;
private readonly IUmbracoJsonTypeInfoResolver _umbracoJsonTypeInfoResolver;
[Obsolete("The settings parameter is not required anymore, use the other constructor instead. Scheduled for removal in Umbraco 17.")]
public SubTypesSelector(
IOptions<GlobalSettings> settings,
IHostingEnvironment hostingEnvironment,
IHttpContextAccessor httpContextAccessor,
IEnumerable<ISubTypesHandler> subTypeHandlers,
IUmbracoJsonTypeInfoResolver umbracoJsonTypeInfoResolver)
{
_hostingEnvironment = hostingEnvironment;
_httpContextAccessor = httpContextAccessor;
_subTypeHandlers = subTypeHandlers;
_umbracoJsonTypeInfoResolver = umbracoJsonTypeInfoResolver;
}
public SubTypesSelector(
IHostingEnvironment hostingEnvironment,
IHttpContextAccessor httpContextAccessor,
@@ -3,12 +3,13 @@ using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Options;
using Microsoft.OpenApi;
using Microsoft.OpenApi.Models;
using Swashbuckle.AspNetCore.SwaggerGen;
using Swashbuckle.AspNetCore.SwaggerUI;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Hosting;
using Umbraco.Cms.Web.Common.ApplicationBuilder;
using Umbraco.Extensions;
using IHostingEnvironment = Umbraco.Cms.Core.Hosting.IHostingEnvironment;
namespace Umbraco.Cms.Api.Common.OpenApi;
@@ -16,6 +16,11 @@
<PackageReference Include="OpenIddict.AspNetCore" />
</ItemGroup>
<ItemGroup>
<!-- Take top-level depedendency on OpenIddict.AspNetCore depends on a vulnerable version -->
<PackageReference Include="Microsoft.Extensions.Caching.Memory" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Umbraco.Core\Umbraco.Core.csproj" />
<ProjectReference Include="..\Umbraco.Web.Common\Umbraco.Web.Common.csproj" />
@@ -1,6 +1,6 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using Microsoft.OpenApi;
using Microsoft.OpenApi.Models;
using Swashbuckle.AspNetCore.SwaggerGen;
using Umbraco.Cms.Api.Common.OpenApi;
using Umbraco.Cms.Api.Delivery.Filters;
@@ -1,6 +1,6 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using Microsoft.OpenApi;
using Microsoft.OpenApi.Models;
using Swashbuckle.AspNetCore.SwaggerGen;
using Umbraco.Cms.Api.Common.Security;
using Umbraco.Cms.Api.Delivery.Controllers.Content;
@@ -35,9 +35,23 @@ public class ConfigureUmbracoMemberAuthenticationDeliveryApiSwaggerGenOptions :
return;
}
var schemaRef = new OpenApiSecuritySchemeReference(AuthSchemeName, context.Document);
operation.Security ??= new List<OpenApiSecurityRequirement>();
operation.Security.Add(new OpenApiSecurityRequirement { [schemaRef] = [] });
operation.Security = new List<OpenApiSecurityRequirement>
{
new OpenApiSecurityRequirement
{
{
new OpenApiSecurityScheme
{
Reference = new OpenApiReference
{
Type = ReferenceType.SecurityScheme,
Id = AuthSchemeName,
}
},
[]
}
}
};
}
public void Apply(OpenApiDocument swaggerDoc, DocumentFilterContext context)
@@ -47,7 +61,7 @@ public class ConfigureUmbracoMemberAuthenticationDeliveryApiSwaggerGenOptions :
return;
}
swaggerDoc.AddComponent(
swaggerDoc.Components.SecuritySchemes.Add(
AuthSchemeName,
new OpenApiSecurityScheme
{
@@ -60,9 +74,9 @@ public class ConfigureUmbracoMemberAuthenticationDeliveryApiSwaggerGenOptions :
AuthorizationCode = new OpenApiOAuthFlow
{
AuthorizationUrl = new Uri(Paths.MemberApi.AuthorizationEndpoint, UriKind.Relative),
TokenUrl = new Uri(Paths.MemberApi.TokenEndpoint, UriKind.Relative),
},
},
TokenUrl = new Uri(Paths.MemberApi.TokenEndpoint, UriKind.Relative)
}
}
});
}
}
@@ -1,9 +1,9 @@
using System.Text.Json.Nodes;
using Microsoft.OpenApi;
using Microsoft.OpenApi.Any;
using Microsoft.OpenApi.Models;
using Swashbuckle.AspNetCore.SwaggerGen;
using Umbraco.Cms.Api.Delivery.Configuration;
using Umbraco.Cms.Api.Delivery.Controllers;
using Umbraco.Cms.Api.Delivery.Controllers.Content;
using Umbraco.Cms.Core;
namespace Umbraco.Cms.Api.Delivery.Filters;
@@ -13,7 +13,7 @@ internal sealed class SwaggerContentDocumentationFilter : SwaggerDocumentationFi
protected override void ApplyOperation(OpenApiOperation operation, OperationFilterContext context)
{
operation.Parameters ??= new List<IOpenApiParameter>();
operation.Parameters ??= new List<OpenApiParameter>();
AddExpand(operation, context);
@@ -21,50 +21,50 @@ internal sealed class SwaggerContentDocumentationFilter : SwaggerDocumentationFi
operation.Parameters.Add(new OpenApiParameter
{
Name = Constants.DeliveryApi.HeaderNames.AcceptLanguage,
Name = Core.Constants.DeliveryApi.HeaderNames.AcceptLanguage,
In = ParameterLocation.Header,
Required = false,
Description = "Defines the language to return. Use this when querying language variant content items.",
Schema = new OpenApiSchema { Type = JsonSchemaType.String },
Examples = new Dictionary<string, IOpenApiExample>
Schema = new OpenApiSchema { Type = "string" },
Examples = new Dictionary<string, OpenApiExample>
{
{ "Default", new OpenApiExample { Value = string.Empty } },
{ "English culture", new OpenApiExample { Value = "en-us" } },
},
{ "Default", new OpenApiExample { Value = new OpenApiString(string.Empty) } },
{ "English culture", new OpenApiExample { Value = new OpenApiString("en-us") } }
}
});
operation.Parameters.Add(new OpenApiParameter
{
Name = Constants.DeliveryApi.HeaderNames.AcceptSegment,
Name = Core.Constants.DeliveryApi.HeaderNames.AcceptSegment,
In = ParameterLocation.Header,
Required = false,
Description = "Defines the segment to return. Use this when querying segment variant content items.",
Schema = new OpenApiSchema { Type = JsonSchemaType.String },
Examples = new Dictionary<string, IOpenApiExample>
Schema = new OpenApiSchema { Type = "string" },
Examples = new Dictionary<string, OpenApiExample>
{
{ "Default", new OpenApiExample { Value = string.Empty } },
{ "Segment One", new OpenApiExample { Value = "segment-one" } },
},
{ "Default", new OpenApiExample { Value = new OpenApiString(string.Empty) } },
{ "Segment One", new OpenApiExample { Value = new OpenApiString("segment-one") } }
}
});
AddApiKey(operation);
operation.Parameters.Add(new OpenApiParameter
{
Name = Constants.DeliveryApi.HeaderNames.Preview,
Name = Core.Constants.DeliveryApi.HeaderNames.Preview,
In = ParameterLocation.Header,
Required = false,
Description = "Whether to request draft content.",
Schema = new OpenApiSchema { Type = JsonSchemaType.Boolean },
Schema = new OpenApiSchema { Type = "boolean" }
});
operation.Parameters.Add(new OpenApiParameter
{
Name = Constants.DeliveryApi.HeaderNames.StartItem,
Name = Core.Constants.DeliveryApi.HeaderNames.StartItem,
In = ParameterLocation.Header,
Required = false,
Description = "URL segment or GUID of a root content item.",
Schema = new OpenApiSchema { Type = JsonSchemaType.String },
Schema = new OpenApiSchema { Type = "string" }
});
}
@@ -92,36 +92,105 @@ internal sealed class SwaggerContentDocumentationFilter : SwaggerDocumentationFi
}
}
private Dictionary<string, IOpenApiExample> FetchQueryParameterExamples() =>
private Dictionary<string, OpenApiExample> FetchQueryParameterExamples() =>
new()
{
{ "Select all", new OpenApiExample { Value = string.Empty } },
{ "Select all ancestors of a node by id", new OpenApiExample { Value = "ancestors:id" } },
{ "Select all ancestors of a node by path", new OpenApiExample { Value = "ancestors:path" } },
{ "Select all children of a node by id", new OpenApiExample { Value = "children:id" } },
{ "Select all children of a node by path", new OpenApiExample { Value = "children:path" } },
{ "Select all descendants of a node by id", new OpenApiExample { Value = "descendants:id" } },
{ "Select all descendants of a node by path", new OpenApiExample { Value = "descendants:path" } },
{ "Select all", new OpenApiExample { Value = new OpenApiString(string.Empty) } },
{
"Select all ancestors of a node by id",
new OpenApiExample { Value = new OpenApiString("ancestors:id") }
},
{
"Select all ancestors of a node by path",
new OpenApiExample { Value = new OpenApiString("ancestors:path") }
},
{
"Select all children of a node by id",
new OpenApiExample { Value = new OpenApiString("children:id") }
},
{
"Select all children of a node by path",
new OpenApiExample { Value = new OpenApiString("children:path") }
},
{
"Select all descendants of a node by id",
new OpenApiExample { Value = new OpenApiString("descendants:id") }
},
{
"Select all descendants of a node by path",
new OpenApiExample { Value = new OpenApiString("descendants:path") }
}
};
private Dictionary<string, IOpenApiExample> FilterQueryParameterExamples() =>
private Dictionary<string, OpenApiExample> FilterQueryParameterExamples() =>
new()
{
{ "Default filter", new OpenApiExample { Value = string.Empty } },
{ "Filter by content type (equals)", new OpenApiExample { Value = new JsonArray { "contentType:alias1" } } },
{ "Filter by name (contains)", new OpenApiExample { Value = new JsonArray { "name:nodeName" } } },
{ "Filter by creation date (less than)", new OpenApiExample { Value = new JsonArray { "createDate<2024-01-01" } } },
{ "Filter by update date (greater than or equal)", new OpenApiExample { Value = new JsonArray { "updateDate>:2023-01-01" } } },
{ "Default filter", new OpenApiExample { Value = new OpenApiString(string.Empty) } },
{
"Filter by content type (equals)",
new OpenApiExample { Value = new OpenApiArray { new OpenApiString("contentType:alias1") } }
},
{
"Filter by name (contains)",
new OpenApiExample { Value = new OpenApiArray { new OpenApiString("name:nodeName") } }
},
{
"Filter by creation date (less than)",
new OpenApiExample { Value = new OpenApiArray { new OpenApiString("createDate<2024-01-01") } }
},
{
"Filter by update date (greater than or equal)",
new OpenApiExample { Value = new OpenApiArray { new OpenApiString("updateDate>:2023-01-01") } }
}
};
private Dictionary<string, IOpenApiExample> SortQueryParameterExamples() =>
private Dictionary<string, OpenApiExample> SortQueryParameterExamples() =>
new()
{
{ "Default sort", new OpenApiExample { Value = string.Empty } },
{ "Sort by create date", new OpenApiExample { Value = new JsonArray { "createDate:asc", "createDate:desc" } } },
{ "Sort by level", new OpenApiExample { Value = new JsonArray { "level:asc", "level:desc" } } },
{ "Sort by name", new OpenApiExample { Value = new JsonArray { "name:asc", "name:desc" } } },
{ "Sort by sort order", new OpenApiExample { Value = new JsonArray { "sortOrder:asc", "sortOrder:desc" } } },
{ "Sort by update date", new OpenApiExample { Value = new JsonArray { "updateDate:asc", "updateDate:desc" } } },
{ "Default sort", new OpenApiExample { Value = new OpenApiString(string.Empty) } },
{
"Sort by create date",
new OpenApiExample
{
Value = new OpenApiArray
{
new OpenApiString("createDate:asc"), new OpenApiString("createDate:desc")
}
}
},
{
"Sort by level",
new OpenApiExample
{
Value = new OpenApiArray { new OpenApiString("level:asc"), new OpenApiString("level:desc") }
}
},
{
"Sort by name",
new OpenApiExample
{
Value = new OpenApiArray { new OpenApiString("name:asc"), new OpenApiString("name:desc") }
}
},
{
"Sort by sort order",
new OpenApiExample
{
Value = new OpenApiArray
{
new OpenApiString("sortOrder:asc"), new OpenApiString("sortOrder:desc")
}
}
},
{
"Sort by update date",
new OpenApiExample
{
Value = new OpenApiArray
{
new OpenApiString("updateDate:asc"), new OpenApiString("updateDate:desc")
}
}
}
};
}
@@ -1,7 +1,7 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.OpenApi;
using Microsoft.OpenApi.Any;
using Microsoft.OpenApi.Models;
using Swashbuckle.AspNetCore.SwaggerGen;
using Umbraco.Cms.Core;
namespace Umbraco.Cms.Api.Delivery.Filters;
@@ -9,8 +9,6 @@ internal abstract class SwaggerDocumentationFilterBase<TBaseController>
: SwaggerFilterBase<TBaseController>, IOperationFilter, IParameterFilter
where TBaseController : Controller
{
protected abstract string DocumentationLink { get; }
public void Apply(OpenApiOperation operation, OperationFilterContext context)
{
if (CanApply(context))
@@ -19,19 +17,21 @@ internal abstract class SwaggerDocumentationFilterBase<TBaseController>
}
}
public void Apply(IOpenApiParameter parameter, ParameterFilterContext context)
public void Apply(OpenApiParameter parameter, ParameterFilterContext context)
{
if (CanApply(context) && parameter is OpenApiParameter openApiParameter)
if (CanApply(context))
{
ApplyParameter(openApiParameter, context);
ApplyParameter(parameter, context);
}
}
protected abstract string DocumentationLink { get; }
protected abstract void ApplyOperation(OpenApiOperation operation, OperationFilterContext context);
protected abstract void ApplyParameter(OpenApiParameter parameter, ParameterFilterContext context);
protected void AddQueryParameterDocumentation(OpenApiParameter parameter, Dictionary<string, IOpenApiExample> examples, string description)
protected void AddQueryParameterDocumentation(OpenApiParameter parameter, Dictionary<string, OpenApiExample> examples, string description)
{
parameter.Description = QueryParameterDescription(description);
parameter.Examples = examples;
@@ -60,19 +60,15 @@ internal abstract class SwaggerDocumentationFilterBase<TBaseController>
AddFields(operation);
}
protected void AddApiKey(OpenApiOperation operation)
{
operation.Parameters ??= new List<IOpenApiParameter>();
operation.Parameters.Add(
new OpenApiParameter
{
Name = Constants.DeliveryApi.HeaderNames.ApiKey,
In = ParameterLocation.Header,
Required = false,
Description = "API key specified through configuration to authorize access to the API.",
Schema = new OpenApiSchema { Type = JsonSchemaType.String },
});
}
protected void AddApiKey(OpenApiOperation operation) =>
operation.Parameters.Add(new OpenApiParameter
{
Name = Core.Constants.DeliveryApi.HeaderNames.ApiKey,
In = ParameterLocation.Header,
Required = false,
Description = "API key specified through configuration to authorize access to the API.",
Schema = new OpenApiSchema { Type = "string" }
});
protected string PaginationDescription(bool skip, string itemType)
=> $"Specifies the number of found {itemType} items to {(skip ? "skip" : "take")}. Use this to control pagination of the response.";
@@ -86,70 +82,78 @@ internal abstract class SwaggerDocumentationFilterBase<TBaseController>
// FIXME: remove this when Delivery API V1 has been removed (expectedly in V15)
private void AddExpandV1(OpenApiOperation operation)
{
operation.Parameters ??= new List<IOpenApiParameter>();
operation.Parameters.Add(
new OpenApiParameter
=> operation.Parameters.Add(new OpenApiParameter
{
Name = "expand",
In = ParameterLocation.Query,
Required = false,
Description = QueryParameterDescription("Defines the properties that should be expanded in the response"),
Schema = new OpenApiSchema { Type = "string" },
Examples = new Dictionary<string, OpenApiExample>
{
Name = "expand",
In = ParameterLocation.Query,
Required = false,
Description =
QueryParameterDescription("Defines the properties that should be expanded in the response"),
Schema = new OpenApiSchema { Type = JsonSchemaType.String },
Examples = new Dictionary<string, IOpenApiExample>
{ "Expand none", new OpenApiExample { Value = new OpenApiString(string.Empty) } },
{ "Expand all", new OpenApiExample { Value = new OpenApiString("all") } },
{
{ "Expand none", new OpenApiExample { Value = string.Empty } },
{ "Expand all", new OpenApiExample { Value = "all" } },
{ "Expand specific property", new OpenApiExample { Value = "property:alias1" } },
{ "Expand specific properties", new OpenApiExample { Value = "property:alias1,alias2" } },
"Expand specific property",
new OpenApiExample { Value = new OpenApiString("property:alias1") }
},
});
}
{
"Expand specific properties",
new OpenApiExample { Value = new OpenApiString("property:alias1,alias2") }
}
}
});
private void AddExpand(OpenApiOperation operation)
{
operation.Parameters ??= new List<IOpenApiParameter>();
operation.Parameters.Add(
new OpenApiParameter
=> operation.Parameters.Add(new OpenApiParameter
{
Name = "expand",
In = ParameterLocation.Query,
Required = false,
Description = QueryParameterDescription("Defines the properties that should be expanded in the response"),
Schema = new OpenApiSchema { Type = "string" },
Examples = new Dictionary<string, OpenApiExample>
{
Name = "expand",
In = ParameterLocation.Query,
Required = false,
Description =
QueryParameterDescription("Defines the properties that should be expanded in the response"),
Schema = new OpenApiSchema { Type = JsonSchemaType.String },
Examples = new Dictionary<string, IOpenApiExample>
{ "Expand none", new OpenApiExample { Value = new OpenApiString(string.Empty) } },
{ "Expand all properties", new OpenApiExample { Value = new OpenApiString("properties[$all]") } },
{
{ "Expand none", new OpenApiExample { Value = string.Empty } },
{ "Expand all properties", new OpenApiExample { Value = "properties[$all]" } },
{ "Expand specific property", new OpenApiExample { Value = "properties[alias1]" } },
{ "Expand specific properties", new OpenApiExample { Value = "properties[alias1,alias2]" } },
{ "Expand nested properties", new OpenApiExample { Value = "properties[alias1[properties[nestedAlias1,nestedAlias2]]]" } },
"Expand specific property",
new OpenApiExample { Value = new OpenApiString("properties[alias1]") }
},
});
}
{
"Expand specific properties",
new OpenApiExample { Value = new OpenApiString("properties[alias1,alias2]") }
},
{
"Expand nested properties",
new OpenApiExample { Value = new OpenApiString("properties[alias1[properties[nestedAlias1,nestedAlias2]]]") }
}
}
});
private void AddFields(OpenApiOperation operation)
{
operation.Parameters ??= new List<IOpenApiParameter>();
operation.Parameters.Add(
new OpenApiParameter
=> operation.Parameters.Add(new OpenApiParameter
{
Name = "fields",
In = ParameterLocation.Query,
Required = false,
Description = QueryParameterDescription("Explicitly defines which properties should be included in the response (by default all properties are included)"),
Schema = new OpenApiSchema { Type = "string" },
Examples = new Dictionary<string, OpenApiExample>
{
Name = "fields",
In = ParameterLocation.Query,
Required = false,
Description =
QueryParameterDescription(
"Explicitly defines which properties should be included in the response (by default all properties are included)"),
Schema = new OpenApiSchema { Type = JsonSchemaType.String },
Examples = new Dictionary<string, IOpenApiExample>
{ "Include all properties", new OpenApiExample { Value = new OpenApiString("properties[$all]") } },
{
{ "Include all properties", new OpenApiExample { Value = "properties[$all]" } },
{ "Include only specific property", new OpenApiExample { Value = "properties[alias1]" } },
{ "Include only specific properties", new OpenApiExample { Value = "properties[alias1,alias2]" } },
{ "Include only specific nested properties", new OpenApiExample { Value = "properties[alias1[properties[nestedAlias1,nestedAlias2]]]" } },
"Include only specific property",
new OpenApiExample { Value = new OpenApiString("properties[alias1]") }
},
});
}
{
"Include only specific properties",
new OpenApiExample { Value = new OpenApiString("properties[alias1,alias2]") }
},
{
"Include only specific nested properties",
new OpenApiExample { Value = new OpenApiString("properties[alias1[properties[nestedAlias1,nestedAlias2]]]") }
}
}
});
}
@@ -1,7 +1,8 @@
using System.Text.Json.Nodes;
using Microsoft.OpenApi;
using Microsoft.OpenApi.Any;
using Microsoft.OpenApi.Models;
using Swashbuckle.AspNetCore.SwaggerGen;
using Umbraco.Cms.Api.Delivery.Configuration;
using Umbraco.Cms.Api.Delivery.Controllers;
using Umbraco.Cms.Api.Delivery.Controllers.Media;
namespace Umbraco.Cms.Api.Delivery.Filters;
@@ -12,7 +13,7 @@ internal sealed class SwaggerMediaDocumentationFilter : SwaggerDocumentationFilt
protected override void ApplyOperation(OpenApiOperation operation, OperationFilterContext context)
{
operation.Parameters ??= new List<IOpenApiParameter>();
operation.Parameters ??= new List<OpenApiParameter>();
AddExpand(operation, context);
@@ -45,29 +46,77 @@ internal sealed class SwaggerMediaDocumentationFilter : SwaggerDocumentationFilt
}
}
private Dictionary<string, IOpenApiExample> FetchQueryParameterExamples() =>
private Dictionary<string, OpenApiExample> FetchQueryParameterExamples() =>
new()
{
{ "Select all children at root level", new OpenApiExample { Value = "children:/" } },
{ "Select all children of a media item by id", new OpenApiExample { Value = "children:id" } },
{ "Select all children of a media item by path", new OpenApiExample { Value = "children:path" } },
{
"Select all children at root level",
new OpenApiExample { Value = new OpenApiString("children:/") }
},
{
"Select all children of a media item by id",
new OpenApiExample { Value = new OpenApiString("children:id") }
},
{
"Select all children of a media item by path",
new OpenApiExample { Value = new OpenApiString("children:path") }
}
};
private Dictionary<string, IOpenApiExample> FilterQueryParameterExamples() =>
private Dictionary<string, OpenApiExample> FilterQueryParameterExamples() =>
new()
{
{ "Default filter", new OpenApiExample { Value = string.Empty } },
{ "Filter by media type", new OpenApiExample { Value = new JsonArray { "mediaType:alias1" } } },
{ "Filter by name", new OpenApiExample { Value = new JsonArray { "name:nodeName" } } },
{ "Default filter", new OpenApiExample { Value = new OpenApiString(string.Empty) } },
{
"Filter by media type",
new OpenApiExample { Value = new OpenApiArray { new OpenApiString("mediaType:alias1") } }
},
{
"Filter by name",
new OpenApiExample { Value = new OpenApiArray { new OpenApiString("name:nodeName") } }
}
};
private Dictionary<string, IOpenApiExample> SortQueryParameterExamples() =>
private Dictionary<string, OpenApiExample> SortQueryParameterExamples() =>
new()
{
{ "Default sort", new OpenApiExample { Value = string.Empty } },
{ "Sort by create date", new OpenApiExample { Value = new JsonArray { "createDate:asc", "createDate:desc" } } },
{ "Sort by name", new OpenApiExample { Value = new JsonArray { "name:asc", "name:desc" } } },
{ "Sort by sort order", new OpenApiExample { Value = new JsonArray { "sortOrder:asc", "sortOrder:desc" } } },
{ "Sort by update date", new OpenApiExample { Value = new JsonArray { "updateDate:asc", "updateDate:desc" } } },
{ "Default sort", new OpenApiExample { Value = new OpenApiString(string.Empty) } },
{
"Sort by create date",
new OpenApiExample
{
Value = new OpenApiArray
{
new OpenApiString("createDate:asc"), new OpenApiString("createDate:desc")
}
}
},
{
"Sort by name",
new OpenApiExample
{
Value = new OpenApiArray { new OpenApiString("name:asc"), new OpenApiString("name:desc") }
}
},
{
"Sort by sort order",
new OpenApiExample
{
Value = new OpenApiArray
{
new OpenApiString("sortOrder:asc"), new OpenApiString("sortOrder:desc")
}
}
},
{
"Sort by update date",
new OpenApiExample
{
Value = new OpenApiArray
{
new OpenApiString("updateDate:asc"), new OpenApiString("updateDate:desc")
}
}
}
};
}
@@ -1,5 +1,8 @@
using Microsoft.Extensions.DependencyInjection;
using Umbraco.Cms.Core.DeliveryApi;
using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Core.Models.PublishedContent;
using Umbraco.Cms.Core.PublishedCache;
using Umbraco.Extensions;
namespace Umbraco.Cms.Api.Delivery.Querying;
@@ -11,6 +14,30 @@ public abstract class QueryOptionBase
private readonly IApiDocumentUrlService _apiDocumentUrlService;
private readonly IVariationContextAccessor _variationContextAccessor;
[Obsolete("Please use the non-obsolete constructor. Will be removed in V17.")]
public QueryOptionBase(
IPublishedContentCache publishedContentCache,
IRequestRoutingService requestRoutingService)
: this(
requestRoutingService,
StaticServiceProvider.Instance.GetRequiredService<IRequestPreviewService>(),
StaticServiceProvider.Instance.GetRequiredService<IApiDocumentUrlService>(),
StaticServiceProvider.Instance.GetRequiredService<IVariationContextAccessor>())
{
}
[Obsolete("Please use the non-obsolete constructor. Will be removed in V17.")]
public QueryOptionBase(
IPublishedContentCache publishedContentCache,
IRequestRoutingService requestRoutingService,
IRequestPreviewService requestPreviewService,
IRequestCultureService requestCultureService,
IApiDocumentUrlService apiDocumentUrlService,
IVariationContextAccessor variationContextAccessor)
: this(requestRoutingService, requestPreviewService, apiDocumentUrlService, variationContextAccessor)
{
}
public QueryOptionBase(
IRequestRoutingService requestRoutingService,
IRequestPreviewService requestPreviewService,
@@ -1,6 +1,9 @@
using Microsoft.Extensions.DependencyInjection;
using Umbraco.Cms.Api.Delivery.Indexing.Selectors;
using Umbraco.Cms.Core.DeliveryApi;
using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Core.Models.PublishedContent;
using Umbraco.Cms.Core.PublishedCache;
using Umbraco.Cms.Core.Services.Navigation;
namespace Umbraco.Cms.Api.Delivery.Querying.Selectors;
@@ -10,6 +13,46 @@ public sealed class AncestorsSelector : QueryOptionBase, ISelectorHandler
private readonly IDocumentNavigationQueryService _navigationQueryService;
private const string AncestorsSpecifier = "ancestors:";
[Obsolete("Please use the non-obsolete constructor. Will be removed in V17.")]
public AncestorsSelector(
IPublishedContentCache publishedContentCache,
IRequestRoutingService requestRoutingService,
IDocumentNavigationQueryService navigationQueryService,
IRequestPreviewService requestPreviewService)
: this(
requestRoutingService,
requestPreviewService,
StaticServiceProvider.Instance.GetRequiredService<IApiDocumentUrlService>(),
StaticServiceProvider.Instance.GetRequiredService<IVariationContextAccessor>(),
navigationQueryService)
{
}
[Obsolete("Please use the non-obsolete constructor. Will be removed in V17.")]
public AncestorsSelector(
IPublishedContentCache publishedContentCache,
IRequestRoutingService requestRoutingService,
IDocumentNavigationQueryService navigationQueryService)
: this(
requestRoutingService,
StaticServiceProvider.Instance.GetRequiredService<IRequestPreviewService>(),
StaticServiceProvider.Instance.GetRequiredService<IApiDocumentUrlService>(),
StaticServiceProvider.Instance.GetRequiredService<IVariationContextAccessor>(),
navigationQueryService)
{
}
[Obsolete("Use the constructor that takes all parameters. Scheduled for removal in V17.")]
public AncestorsSelector(IPublishedContentCache publishedContentCache, IRequestRoutingService requestRoutingService)
: this(
requestRoutingService,
StaticServiceProvider.Instance.GetRequiredService<IRequestPreviewService>(),
StaticServiceProvider.Instance.GetRequiredService<IApiDocumentUrlService>(),
StaticServiceProvider.Instance.GetRequiredService<IVariationContextAccessor>(),
StaticServiceProvider.Instance.GetRequiredService<IDocumentNavigationQueryService>())
{
}
public AncestorsSelector(
IRequestRoutingService requestRoutingService,
IRequestPreviewService requestPreviewService,
@@ -19,6 +62,18 @@ public sealed class AncestorsSelector : QueryOptionBase, ISelectorHandler
: base(requestRoutingService, requestPreviewService, apiDocumentUrlService, variationContextAccessor)
=> _navigationQueryService = navigationQueryService;
[Obsolete("Use the constructor that takes all parameters. Scheduled for removal in V17.")]
public AncestorsSelector(
IRequestRoutingService requestRoutingService,
IPublishedContentCache publishedContentCache,
IRequestPreviewService requestPreviewService,
IApiDocumentUrlService apiDocumentUrlService,
IVariationContextAccessor variationContextAccessor,
IDocumentNavigationQueryService navigationQueryService)
: this(requestRoutingService, requestPreviewService, apiDocumentUrlService, variationContextAccessor, navigationQueryService)
{
}
/// <inheritdoc />
public bool CanHandle(string query)
=> query.StartsWith(AncestorsSpecifier, StringComparison.OrdinalIgnoreCase);
@@ -12,6 +12,27 @@ public sealed class ChildrenSelector : QueryOptionBase, ISelectorHandler
{
private const string ChildrenSpecifier = "children:";
[Obsolete("Please use the non-obsolete constructor. Will be removed in V17.")]
public ChildrenSelector(IPublishedContentCache publishedContentCache, IRequestRoutingService requestRoutingService)
: this(
requestRoutingService,
StaticServiceProvider.Instance.GetRequiredService<IRequestPreviewService>(),
StaticServiceProvider.Instance.GetRequiredService<IApiDocumentUrlService>(),
StaticServiceProvider.Instance.GetRequiredService<IVariationContextAccessor>())
{
}
[Obsolete("Please use the non-obsolete constructor. Will be removed in V17.")]
public ChildrenSelector(
IPublishedContentCache publishedContentCache,
IRequestRoutingService requestRoutingService,
IRequestPreviewService requestPreviewService,
IApiDocumentUrlService apiDocumentUrlService,
IVariationContextAccessor variationContextAccessor)
: this(requestRoutingService, requestPreviewService, apiDocumentUrlService, variationContextAccessor)
{
}
public ChildrenSelector(
IRequestRoutingService requestRoutingService,
IRequestPreviewService requestPreviewService,
@@ -12,6 +12,27 @@ public sealed class DescendantsSelector : QueryOptionBase, ISelectorHandler
{
private const string DescendantsSpecifier = "descendants:";
[Obsolete("Please use the non-obsolete constructor. Will be removed in V17.")]
public DescendantsSelector(IPublishedContentCache publishedContentCache, IRequestRoutingService requestRoutingService)
: this(
requestRoutingService,
StaticServiceProvider.Instance.GetRequiredService<IRequestPreviewService>(),
StaticServiceProvider.Instance.GetRequiredService<IApiDocumentUrlService>(),
StaticServiceProvider.Instance.GetRequiredService<IVariationContextAccessor>())
{
}
[Obsolete("Please use the non-obsolete constructor. Will be removed in V17.")]
public DescendantsSelector(
IPublishedContentCache publishedContentCache,
IRequestRoutingService requestRoutingService,
IRequestPreviewService requestPreviewService,
IApiDocumentUrlService apiDocumentUrlService,
IVariationContextAccessor variationContextAccessor)
: this(requestRoutingService, requestPreviewService, apiDocumentUrlService, variationContextAccessor)
{
}
public DescendantsSelector(
IRequestRoutingService requestRoutingService,
IRequestPreviewService requestPreviewService,
@@ -66,14 +66,9 @@ internal sealed class DeliveryApiItemsEndpointsMatcherPolicy : MatcherPolicy, IE
{
ApiVersion[]? supportedApiVersions = endpoint.Metadata.GetMetadata<MapToApiVersionAttribute>()?.Versions.ToArray();
// If the endpoint is versioned, the requested API version must be among the API versions supported by the endpoint.
// If the endpoint is NOT versioned, it cannot be used with a requested API version.
if (supportedApiVersions is not null && requestedApiVersion is not null)
{
return supportedApiVersions.Contains(requestedApiVersion);
}
return requestedApiVersion is null;
// if the endpoint is versioned, the requested API version must be among the API versions supported by the endpoint.
// if the endpoint is NOT versioned, it cannot be used with a requested API version
return supportedApiVersions?.Contains(requestedApiVersion) ?? requestedApiVersion is null;
}
private static bool IsByIdsController(ControllerActionDescriptor? controllerActionDescriptor)
@@ -66,7 +66,7 @@ internal sealed class ApiMediaQueryService : IApiMediaQueryService
private IPublishedContent? TryGetByPath(string path, IPublishedMediaCache mediaCache)
{
var segments = path.Split(Constants.CharArrays.ForwardSlash, StringSplitOptions.RemoveEmptyEntries);
IEnumerable<IPublishedContent> currentChildren = GetRootContent(mediaCache);
IEnumerable<IPublishedContent> currentChildren = mediaCache.GetAtRoot();
IPublishedContent? resolvedMedia = null;
foreach (var segment in segments)
@@ -103,7 +103,7 @@ internal sealed class ApiMediaQueryService : IApiMediaQueryService
IPublishedMediaCache mediaCache = GetRequiredPublishedMediaCache();
if (childrenOf.Trim(Constants.CharArrays.ForwardSlash).Length == 0)
{
return GetRootContent(mediaCache);
return mediaCache.GetAtRoot();
}
IPublishedContent? parent = Guid.TryParse(childrenOf, out Guid parentKey)
@@ -196,8 +196,4 @@ internal sealed class ApiMediaQueryService : IApiMediaQueryService
return Attempt.SucceedWithStatus(ApiMediaQueryOperationStatus.Success, result);
}
private IEnumerable<IPublishedContent> GetRootContent(IPublishedMediaCache mediaCache)
=> _mediaNavigationQueryService.TryGetRootKeys(out IEnumerable<Guid> rootKeys) is false ? []
: rootKeys.Select(x => mediaCache.GetById(false, x)).WhereNotNull();
}
@@ -19,6 +19,12 @@ internal sealed partial class RequestCultureService : RequestHeaderHandler, IReq
return ValidLanguageHeaderRegex().IsMatch(acceptLanguage) ? acceptLanguage : null;
}
[Obsolete("Use IVariationContextAccessor to manipulate the variation context. Scheduled for removal in V17.")]
public void SetRequestCulture(string culture)
{
// no-op
}
// at the time of writing we're introducing this to get rid of accept-language header values like "en-GB,en-US;q=0.9,en;q=0.8",
// so we don't want to be too restrictive in this regex - keep it simple for now.
[GeneratedRegex(@"^[\w-]*$")]
@@ -3,7 +3,6 @@
<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,18 +11,11 @@
-->
<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" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="System.Linq.Async">
<ExcludeAssets>compile</ExcludeAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleTo">
<_Parameter1>Umbraco.Tests.UnitTests</_Parameter1>
@@ -5,7 +5,6 @@ using Microsoft.AspNetCore.DataProtection;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using Microsoft.Net.Http.Headers;
using Umbraco.Cms.Api.Management.Security;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Configuration.Models;
@@ -220,44 +219,10 @@ public class ConfigureBackOfficeCookieOptions : IConfigureNamedOptions<CookieAut
}
return Task.CompletedTask;
},
// FIXME: We want to change this over to using an attribute on the backoffice controllers
// See this for more: https://github.com/dotnet/aspnetcore/issues/63093#issuecomment-3201530217
OnRedirectToLogin = context =>
{
if (IsXhr(context.Request))
{
context.Response.Headers.Location = context.RedirectUri;
context.Response.StatusCode = 401;
}
else
{
context.Response.Redirect(context.RedirectUri);
}
return Task.CompletedTask;
},
OnRedirectToAccessDenied = context =>
{
if (IsXhr(context.Request))
{
context.Response.Headers.Location = context.RedirectUri;
context.Response.StatusCode = 403;
}
else
{
context.Response.Redirect(context.RedirectUri);
}
return Task.CompletedTask;
},
}
};
}
private bool IsXhr(HttpRequest request) =>
string.Equals(request.Query[HeaderNames.XRequestedWith], "XMLHttpRequest", StringComparison.Ordinal) ||
string.Equals(request.Headers.XRequestedWith, "XMLHttpRequest", StringComparison.Ordinal);
/// <summary>
/// Ensures the ticket is renewed if the <see cref="SecuritySettings.KeepUserLoggedIn" /> is set to true
/// and the current request is for the get user seconds endpoint
@@ -1,8 +1,8 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using Microsoft.OpenApi;
using Microsoft.OpenApi.Models;
using Swashbuckle.AspNetCore.SwaggerGen;
using Umbraco.Cms.Api.Common.Security;
using Umbraco.Cms.Api.Common.OpenApi;
using Umbraco.Cms.Api.Common.Serialization;
using Umbraco.Cms.Api.Management.DependencyInjection;
using Umbraco.Cms.Api.Management.OpenApi;
@@ -11,7 +11,7 @@ namespace Umbraco.Cms.Api.Management.Configuration;
public class ConfigureUmbracoManagementApiSwaggerGenOptions : IConfigureOptions<SwaggerGenOptions>
{
private readonly IUmbracoJsonTypeInfoResolver _umbracoJsonTypeInfoResolver;
private IUmbracoJsonTypeInfoResolver _umbracoJsonTypeInfoResolver;
public ConfigureUmbracoManagementApiSwaggerGenOptions(IUmbracoJsonTypeInfoResolver umbracoJsonTypeInfoResolver)
{
@@ -20,6 +20,7 @@ public class ConfigureUmbracoManagementApiSwaggerGenOptions : IConfigureOptions<
public void Configure(SwaggerGenOptions swaggerGenOptions)
{
swaggerGenOptions.SwaggerDoc(
ManagementApiConfiguration.ApiName,
new OpenApiInfo
@@ -50,10 +51,10 @@ public class ConfigureUmbracoManagementApiSwaggerGenOptions : IConfigureOptions<
AuthorizationCode = new OpenApiOAuthFlow
{
AuthorizationUrl =
new Uri(Paths.BackOfficeApi.AuthorizationEndpoint, UriKind.Relative),
TokenUrl = new Uri(Paths.BackOfficeApi.TokenEndpoint, UriKind.Relative),
},
},
new Uri(Common.Security.Paths.BackOfficeApi.AuthorizationEndpoint, UriKind.Relative),
TokenUrl = new Uri(Common.Security.Paths.BackOfficeApi.TokenEndpoint, UriKind.Relative)
}
}
});
// Sets Security requirement on backoffice apis
@@ -29,6 +29,39 @@ public abstract class ContentCollectionControllerBase<TContent, TCollectionRespo
_flagProviders = flagProvider;
}
[Obsolete("Use the constructer with all parameters. To be removed in Umbraco 18")]
protected ContentCollectionControllerBase(IUmbracoMapper mapper)
: this(mapper, StaticServiceProvider.Instance.GetRequiredService<FlagProviderCollection>())
{
}
[Obsolete("This method is no longer used and will be removed in Umbraco 17.")]
protected IActionResult CollectionResult(ListViewPagedModel<TContent> result)
{
PagedModel<TContent> collectionItemsResult = result.Items;
ListViewConfiguration collectionConfiguration = result.ListViewConfiguration;
var collectionPropertyAliases = collectionConfiguration
.IncludeProperties
.Select(p => p.Alias)
.WhereNotNull()
.ToArray();
List<TCollectionResponseModel> collectionResponseModels =
_mapper.MapEnumerable<TContent, TCollectionResponseModel>(collectionItemsResult.Items, context =>
{
context.SetIncludedProperties(collectionPropertyAliases);
});
var pageViewModel = new PagedViewModel<TCollectionResponseModel>
{
Items = collectionResponseModels,
Total = collectionItemsResult.Total,
};
return Ok(pageViewModel);
}
/// <summary>
/// Creates a collection result from the provided collection response models and total number of items.
/// </summary>
@@ -1,5 +1,7 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Api.Management.ViewModels.Content;
using Umbraco.Cms.Core.Mapping;
using Umbraco.Cms.Core.Models.ContentEditing;
using Umbraco.Cms.Core.Models.ContentEditing.Validation;
using Umbraco.Cms.Core.PropertyEditors.Validation;
@@ -10,6 +12,7 @@ namespace Umbraco.Cms.Api.Management.Controllers.Content;
public abstract class ContentControllerBase : ManagementApiControllerBase
{
protected IActionResult ContentEditingOperationStatusResult(ContentEditingOperationStatus status)
=> OperationStatusResult(status, problemDetailsBuilder => status switch
{
@@ -95,17 +98,6 @@ public abstract class ContentControllerBase : ManagementApiControllerBase
.Build()),
});
protected IActionResult GetReferencesOperationStatusResult(GetReferencesOperationStatus status)
=> OperationStatusResult(status, problemDetailsBuilder => status switch
{
GetReferencesOperationStatus.ContentNotFound => NotFound(problemDetailsBuilder
.WithTitle("The requested content could not be found")
.Build()),
_ => StatusCode(StatusCodes.Status500InternalServerError, problemDetailsBuilder
.WithTitle("Unknown get references operation status.")
.Build()),
});
protected IActionResult ContentEditingOperationStatusResult<TContentModelBase, TValueModel, TVariantModel>(
ContentEditingOperationStatus status,
TContentModelBase requestModel,
@@ -0,0 +1,40 @@
using Asp.Versioning;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Api.Management.Factories;
using Umbraco.Cms.Api.Management.ViewModels.DataType;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Services.OperationStatus;
namespace Umbraco.Cms.Api.Management.Controllers.DataType;
[ApiVersion("1.0")]
[Obsolete("Please use ReferencedByDataTypeController and the referenced-by endpoint. Scheduled for removal in Umbraco 17.")]
public class ReferencesDataTypeController : DataTypeControllerBase
{
private readonly IDataTypeService _dataTypeService;
private readonly IDataTypeReferencePresentationFactory _dataTypeReferencePresentationFactory;
public ReferencesDataTypeController(IDataTypeService dataTypeService, IDataTypeReferencePresentationFactory dataTypeReferencePresentationFactory)
{
_dataTypeService = dataTypeService;
_dataTypeReferencePresentationFactory = dataTypeReferencePresentationFactory;
}
[HttpGet("{id:guid}/references")]
[MapToApiVersion("1.0")]
[ProducesResponseType(typeof(DataTypeReferenceResponseModel[]), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
public async Task<IActionResult> References(CancellationToken cancellationToken, Guid id)
{
Attempt<IReadOnlyDictionary<Udi, IEnumerable<string>>, DataTypeOperationStatus> result = await _dataTypeService.GetReferencesAsync(id);
if (result.Success == false)
{
return DataTypeOperationStatusResult(result.Status);
}
DataTypeReferenceResponseModel[] viewModels = _dataTypeReferencePresentationFactory.CreateDataTypeReferenceViewModels(result.Result).ToArray();
return Ok(viewModels);
}
}
@@ -2,10 +2,13 @@ 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.Actions;
using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Core.Security.Authorization;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Services.Querying;
using Umbraco.Cms.Web.Common.Authorization;
using Umbraco.Extensions;
@@ -19,6 +22,31 @@ public class ByKeyDocumentController : DocumentControllerBase
private readonly IDocumentPresentationFactory _documentPresentationFactory;
private readonly IContentQueryService _contentQueryService;
[Obsolete("Scheduled for removal in v17")]
public ByKeyDocumentController(
IAuthorizationService authorizationService,
IContentEditingService contentEditingService,
IDocumentPresentationFactory documentPresentationFactory)
{
_authorizationService = authorizationService;
_documentPresentationFactory = documentPresentationFactory;
_contentQueryService = StaticServiceProvider.Instance.GetRequiredService<IContentQueryService>();
}
// needed for greedy selection until other constructor remains in v17
[Obsolete("Scheduled for removal in v17")]
public ByKeyDocumentController(
IAuthorizationService authorizationService,
IContentEditingService contentEditingService,
IDocumentPresentationFactory documentPresentationFactory,
IContentQueryService contentQueryService)
{
_authorizationService = authorizationService;
_documentPresentationFactory = documentPresentationFactory;
_contentQueryService = contentQueryService;
}
[ActivatorUtilitiesConstructor]
public ByKeyDocumentController(
IAuthorizationService authorizationService,
IDocumentPresentationFactory documentPresentationFactory,
@@ -23,6 +23,12 @@ public abstract class DocumentCollectionControllerBase : ContentCollectionContro
{
}
[Obsolete("Please use the constructor with all parameters. Scheduled to be removed in V18")]
protected DocumentCollectionControllerBase(IUmbracoMapper mapper)
: base(mapper)
{
}
protected IActionResult CollectionOperationStatusResult(ContentCollectionOperationStatus status)
=> ContentCollectionOperationStatusResult(status, "document");
}
@@ -1,53 +0,0 @@
using Asp.Versioning;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Api.Common.Builders;
using Umbraco.Cms.Api.Management.Factories;
using Umbraco.Cms.Api.Management.ViewModels.Document;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Services;
namespace Umbraco.Cms.Api.Management.Controllers.Document;
[ApiVersion("1.0")]
public class DocumentPreviewUrlController : DocumentControllerBase
{
private readonly IContentService _contentService;
private readonly IDocumentUrlFactory _documentUrlFactory;
public DocumentPreviewUrlController(
IContentService contentService,
IDocumentUrlFactory documentUrlFactory)
{
_contentService = contentService;
_documentUrlFactory = documentUrlFactory;
}
[MapToApiVersion("1.0")]
[HttpGet("{id:guid}/preview-url")]
[ProducesResponseType(typeof(DocumentUrlInfo), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
public async Task<IActionResult> GetPreviewUrl(Guid id, string providerAlias, string? culture, string? segment)
{
IContent? content = _contentService.GetById(id);
if (content is null)
{
return NotFound(new ProblemDetailsBuilder()
.WithTitle("Document not found")
.WithDetail("The requested document did not exist.")
.Build());
}
DocumentUrlInfo? previewUrlInfo = await _documentUrlFactory.GetPreviewUrlAsync(content, providerAlias, culture, segment);
if (previewUrlInfo is null)
{
return BadRequest(new ProblemDetailsBuilder()
.WithTitle("No preview URL for document")
.WithDetail("Failed to produce a preview URL for the requested document.")
.Build());
}
return Ok(previewUrlInfo);
}
}
@@ -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();
@@ -4,10 +4,8 @@ using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Api.Common.ViewModels.Pagination;
using Umbraco.Cms.Api.Management.Factories;
using Umbraco.Cms.Api.Management.ViewModels.TrackedReferences;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Services.OperationStatus;
namespace Umbraco.Cms.Api.Management.Controllers.Document.References;
@@ -17,16 +15,22 @@ public class ReferencedByDocumentController : DocumentControllerBase
private readonly ITrackedReferencesService _trackedReferencesService;
private readonly IRelationTypePresentationFactory _relationTypePresentationFactory;
public ReferencedByDocumentController(
ITrackedReferencesService trackedReferencesService,
IRelationTypePresentationFactory relationTypePresentationFactory)
public ReferencedByDocumentController(ITrackedReferencesService trackedReferencesService, IRelationTypePresentationFactory relationTypePresentationFactory)
{
_trackedReferencesService = trackedReferencesService;
_relationTypePresentationFactory = relationTypePresentationFactory;
}
[Obsolete("Use the ReferencedBy2 action method instead. Scheduled for removal in Umbraco 19, when ReferencedBy2 will be renamed back to ReferencedBy.")]
[NonAction]
/// <summary>
/// Gets a paged list of tracked references for the current item, so you can see where an item is being used.
/// </summary>
/// <remarks>
/// Used by info tabs on content, media etc. and for the delete and unpublish of single items.
/// This is basically finding parents of relations.
/// </remarks>
[HttpGet("{id:guid}/referenced-by")]
[MapToApiVersion("1.0")]
[ProducesResponseType(typeof(PagedViewModel<IReferenceResponseModel>), StatusCodes.Status200OK)]
public async Task<ActionResult<PagedViewModel<IReferenceResponseModel>>> ReferencedBy(
CancellationToken cancellationToken,
Guid id,
@@ -43,37 +47,4 @@ public class ReferencedByDocumentController : DocumentControllerBase
return pagedViewModel;
}
/// <summary>
/// Gets a paged list of tracked references for the current item, so you can see where an item is being used.
/// </summary>
/// <remarks>
/// Used by info tabs on content, media etc. and for the delete and unpublish of single items.
/// This is basically finding parents of relations.
/// </remarks>
[HttpGet("{id:guid}/referenced-by")]
[MapToApiVersion("1.0")]
[ProducesResponseType(typeof(PagedViewModel<IReferenceResponseModel>), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
public async Task<IActionResult> ReferencedBy2(
CancellationToken cancellationToken,
Guid id,
int skip = 0,
int take = 20)
{
Attempt<PagedModel<RelationItemModel>, GetReferencesOperationStatus> relationItemsAttempt = await _trackedReferencesService.GetPagedRelationsForItemAsync(id, UmbracoObjectTypes.Document, skip, take, true);
if (relationItemsAttempt.Success is false)
{
return GetReferencesOperationStatusResult(relationItemsAttempt.Status);
}
var pagedViewModel = new PagedViewModel<IReferenceResponseModel>
{
Total = relationItemsAttempt.Result.Total,
Items = await _relationTypePresentationFactory.CreateReferenceResponseModelsAsync(relationItemsAttempt.Result.Items),
};
return Ok(pagedViewModel);
}
}
@@ -3,11 +3,9 @@ using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Api.Common.ViewModels.Pagination;
using Umbraco.Cms.Api.Management.ViewModels;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Mapping;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Services.OperationStatus;
namespace Umbraco.Cms.Api.Management.Controllers.Document.References;
@@ -17,32 +15,12 @@ public class ReferencedDescendantsDocumentController : DocumentControllerBase
private readonly ITrackedReferencesService _trackedReferencesSkipTakeService;
private readonly IUmbracoMapper _umbracoMapper;
public ReferencedDescendantsDocumentController(
ITrackedReferencesService trackedReferencesSkipTakeService,
IUmbracoMapper umbracoMapper)
public ReferencedDescendantsDocumentController(ITrackedReferencesService trackedReferencesSkipTakeService, IUmbracoMapper umbracoMapper)
{
_trackedReferencesSkipTakeService = trackedReferencesSkipTakeService;
_umbracoMapper = umbracoMapper;
}
[Obsolete("Use the ReferencedDescendants2 action method instead. Scheduled for removal in Umbraco 19, when ReferencedDescendants2 will be renamed back to ReferencedDescendants.")]
[NonAction]
public async Task<ActionResult<PagedViewModel<ReferenceByIdModel>>> ReferencedDescendants(
CancellationToken cancellationToken,
Guid id,
int skip = 0,
int take = 20)
{
PagedModel<RelationItemModel> relationItems = await _trackedReferencesSkipTakeService.GetPagedDescendantsInReferencesAsync(id, skip, take, true);
var pagedViewModel = new PagedViewModel<ReferenceByIdModel>
{
Total = relationItems.Total,
Items = _umbracoMapper.MapEnumerable<RelationItemModel, ReferenceByIdModel>(relationItems.Items),
};
return pagedViewModel;
}
/// <summary>
/// Gets a paged list of the descendant nodes of the current item used in any kind of relation.
/// </summary>
@@ -54,26 +32,19 @@ public class ReferencedDescendantsDocumentController : DocumentControllerBase
[HttpGet("{id:guid}/referenced-descendants")]
[MapToApiVersion("1.0")]
[ProducesResponseType(typeof(PagedViewModel<ReferenceByIdModel>), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
public async Task<IActionResult> ReferencedDescendants2(
public async Task<ActionResult<PagedViewModel<ReferenceByIdModel>>> ReferencedDescendants(
CancellationToken cancellationToken,
Guid id,
int skip = 0,
int take = 20)
{
Attempt<PagedModel<RelationItemModel>, GetReferencesOperationStatus> relationItemsAttempt = await _trackedReferencesSkipTakeService.GetPagedDescendantsInReferencesAsync(id, UmbracoObjectTypes.Document, skip, take, true);
if (relationItemsAttempt.Success is false)
{
return GetReferencesOperationStatusResult(relationItemsAttempt.Status);
}
PagedModel<RelationItemModel> relationItems = await _trackedReferencesSkipTakeService.GetPagedDescendantsInReferencesAsync(id, skip, take, true);
var pagedViewModel = new PagedViewModel<ReferenceByIdModel>
{
Total = relationItemsAttempt.Result.Total,
Items = _umbracoMapper.MapEnumerable<RelationItemModel, ReferenceByIdModel>(relationItemsAttempt.Result.Items),
Total = relationItems.Total,
Items = _umbracoMapper.MapEnumerable<RelationItemModel, ReferenceByIdModel>(relationItems.Items),
};
return Ok(pagedViewModel);
return pagedViewModel;
}
}
@@ -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)
{
@@ -21,6 +21,20 @@ public class ValidateCreateDocumentController : CreateDocumentControllerBase
private readonly IContentEditingService _contentEditingService;
private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor;
[Obsolete("Please use the constructor taking all parameters. Scheduled for removal in Umbraco 17.")]
public ValidateCreateDocumentController(
IAuthorizationService authorizationService,
IDocumentEditingPresentationFactory documentEditingPresentationFactory,
IContentEditingService contentEditingService)
: this(
authorizationService,
documentEditingPresentationFactory,
contentEditingService,
StaticServiceProvider.Instance.GetRequiredService<IBackOfficeSecurityAccessor>())
{
}
[ActivatorUtilitiesConstructor]
public ValidateCreateDocumentController(
IAuthorizationService authorizationService,
IDocumentEditingPresentationFactory documentEditingPresentationFactory,
@@ -21,6 +21,20 @@ public class ValidateUpdateDocumentController : UpdateDocumentControllerBase
private readonly IDocumentEditingPresentationFactory _documentEditingPresentationFactory;
private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor;
[Obsolete("Please use the constructor taking all parameters. Scheduled for removal in Umbraco 17.")]
public ValidateUpdateDocumentController(
IAuthorizationService authorizationService,
IContentEditingService contentEditingService,
IDocumentEditingPresentationFactory documentEditingPresentationFactory)
: this(
authorizationService,
contentEditingService,
documentEditingPresentationFactory,
StaticServiceProvider.Instance.GetRequiredService<IBackOfficeSecurityAccessor>())
{
}
[ActivatorUtilitiesConstructor]
public ValidateUpdateDocumentController(
IAuthorizationService authorizationService,
IContentEditingService contentEditingService,
@@ -16,12 +16,35 @@ public class SearchDocumentTypeItemController : DocumentTypeItemControllerBase
private readonly IUmbracoMapper _mapper;
private readonly IContentTypeSearchService _contentTypeSearchService;
[Obsolete("Please use ctor that only accepts IUmbracoMapper & IContentTypeSearchService, scheduled for removal in v17")]
public SearchDocumentTypeItemController(IEntitySearchService entitySearchService, IContentTypeService contentTypeService, IUmbracoMapper mapper)
: this(mapper, StaticServiceProvider.Instance.GetRequiredService<IContentTypeSearchService>())
{
}
[Obsolete("Please use ctor that only accepts IUmbracoMapper & IContentTypeSearchService, scheduled for removal in v17")]
// We need to have this constructor, or else we get ambiguous constructor error
public SearchDocumentTypeItemController(
IEntitySearchService entitySearchService,
IContentTypeService contentTypeService,
IUmbracoMapper mapper,
IContentTypeSearchService contentTypeSearchService)
: this(mapper, contentTypeSearchService)
{
}
[ActivatorUtilitiesConstructor]
public SearchDocumentTypeItemController(IUmbracoMapper mapper, IContentTypeSearchService contentTypeSearchService)
{
_mapper = mapper;
_contentTypeSearchService = contentTypeSearchService;
}
[NonAction]
[Obsolete("Scheduled to be removed in v16, use the non obsoleted method instead")]
public async Task<IActionResult> Search(CancellationToken cancellationToken, string query, int skip = 0, int take = 100)
=> await SearchDocumentType(cancellationToken, query, null, skip, take);
[HttpGet("search")]
[MapToApiVersion("1.0")]
[ProducesResponseType(typeof(PagedModel<DocumentTypeItemResponseModel>), StatusCodes.Status200OK)]
@@ -11,13 +11,12 @@ using Umbraco.Cms.Api.Common.ViewModels.Pagination;
namespace Umbraco.Cms.Api.Management.Controllers.Help;
[Obsolete("This is no longer used and will be removed in v19")]
[ApiVersion("1.0")]
public class GetHelpController : HelpControllerBase
{
private readonly ILogger<GetHelpController> _logger;
private readonly IJsonSerializer _jsonSerializer;
private readonly HelpPageSettings _helpPageSettings;
private HelpPageSettings _helpPageSettings;
public GetHelpController(
IOptionsMonitor<HelpPageSettings> helpPageSettings,
@@ -27,8 +26,11 @@ public class GetHelpController : HelpControllerBase
_logger = logger;
_jsonSerializer = jsonSerializer;
_helpPageSettings = helpPageSettings.CurrentValue;
helpPageSettings.OnChange(UpdateHelpPageSettings);
}
private void UpdateHelpPageSettings(HelpPageSettings settings) => _helpPageSettings = settings;
[HttpGet]
[MapToApiVersion("1.0")]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)]
@@ -3,7 +3,6 @@ using Umbraco.Cms.Api.Management.Routing;
namespace Umbraco.Cms.Api.Management.Controllers.Help;
[Obsolete("This is no longer used and will be removed in v19")]
[VersionedApiBackOfficeRoute("help")]
[ApiExplorerSettings(GroupName = "Help")]
public abstract class HelpControllerBase : ManagementApiControllerBase
@@ -26,9 +26,9 @@ public class RebuildIndexerController : IndexerControllerBase
}
/// <summary>
/// Rebuilds the index.
/// Rebuilds the index
/// </summary>
/// <param name="indexName">The name of the index to rebuild.</param>
/// <param name="indexName"></param>
/// <returns></returns>
[HttpPost("{indexName}/rebuild")]
[MapToApiVersion("1.0")]
@@ -36,7 +36,7 @@ public class RebuildIndexerController : IndexerControllerBase
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status409Conflict)]
[ProducesResponseType(StatusCodes.Status200OK)]
public async Task<IActionResult> Rebuild(CancellationToken cancellationToken, string indexName)
public Task<IActionResult> Rebuild(CancellationToken cancellationToken, string indexName)
{
if (!_examineManager.TryGetIndex(indexName, out IIndex? index))
{
@@ -48,7 +48,7 @@ public class RebuildIndexerController : IndexerControllerBase
Type = "Error",
};
return NotFound(invalidModelProblem);
return Task.FromResult<IActionResult>(NotFound(invalidModelProblem));
}
if (!_indexingRebuilderService.CanRebuild(index.Name))
@@ -57,19 +57,19 @@ public class RebuildIndexerController : IndexerControllerBase
{
Title = "Could not validate the populator",
Detail =
$"The index {index.Name} could not be rebuilt because we could not validate its associated {typeof(IIndexPopulator)}",
$"The index {index?.Name} could not be rebuilt because we could not validate its associated {typeof(IIndexPopulator)}",
Status = StatusCodes.Status400BadRequest,
Type = "Error",
};
return BadRequest(invalidModelProblem);
return Task.FromResult<IActionResult>(BadRequest(invalidModelProblem));
}
_logger.LogInformation("Rebuilding index '{IndexName}'", indexName);
if (await _indexingRebuilderService.TryRebuildAsync(index, indexName))
if (_indexingRebuilderService.TryRebuild(index, indexName))
{
return Ok();
return Task.FromResult<IActionResult>(Ok());
}
var problemDetails = new ProblemDetails
@@ -80,6 +80,6 @@ public class RebuildIndexerController : IndexerControllerBase
Type = "Error",
};
return Conflict(problemDetails);
return Task.FromResult<IActionResult>(Conflict(problemDetails));
}
}
@@ -26,6 +26,12 @@ public abstract class MediaCollectionControllerBase : ContentCollectionControlle
{
}
[Obsolete("Please use the constructor with all parameters. Scheduled to be removed in Umbraco 18")]
protected MediaCollectionControllerBase(IUmbracoMapper mapper)
: base(mapper)
{
}
protected IActionResult CollectionOperationStatusResult(ContentCollectionOperationStatus status)
=> ContentCollectionOperationStatusResult(status, "media");
}
@@ -4,10 +4,8 @@ using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Api.Common.ViewModels.Pagination;
using Umbraco.Cms.Api.Management.Factories;
using Umbraco.Cms.Api.Management.ViewModels.TrackedReferences;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Services.OperationStatus;
namespace Umbraco.Cms.Api.Management.Controllers.Media.References;
@@ -17,16 +15,22 @@ public class ReferencedByMediaController : MediaControllerBase
private readonly ITrackedReferencesService _trackedReferencesService;
private readonly IRelationTypePresentationFactory _relationTypePresentationFactory;
public ReferencedByMediaController(
ITrackedReferencesService trackedReferencesService,
IRelationTypePresentationFactory relationTypePresentationFactory)
public ReferencedByMediaController(ITrackedReferencesService trackedReferencesService, IRelationTypePresentationFactory relationTypePresentationFactory)
{
_trackedReferencesService = trackedReferencesService;
_relationTypePresentationFactory = relationTypePresentationFactory;
}
[Obsolete("Use the ReferencedBy2 action method instead. Scheduled for removal in Umbraco 19, when ReferencedBy2 will be renamed back to ReferencedBy.")]
[NonAction]
/// <summary>
/// Gets a page list of tracked references for the current item, so you can see where an item is being used.
/// </summary>
/// <remarks>
/// Used by info tabs on content, media etc. and for the delete and unpublish of single items.
/// This is basically finding parents of relations.
/// </remarks>
[HttpGet("{id:guid}/referenced-by")]
[MapToApiVersion("1.0")]
[ProducesResponseType(typeof(PagedViewModel<IReferenceResponseModel>), StatusCodes.Status200OK)]
public async Task<ActionResult<PagedViewModel<IReferenceResponseModel>>> ReferencedBy(
CancellationToken cancellationToken,
Guid id,
@@ -43,37 +47,4 @@ public class ReferencedByMediaController : MediaControllerBase
return pagedViewModel;
}
/// <summary>
/// Gets a page list of tracked references for the current item, so you can see where an item is being used.
/// </summary>
/// <remarks>
/// Used by info tabs on content, media etc. and for the delete and unpublish of single items.
/// This is basically finding parents of relations.
/// </remarks>
[HttpGet("{id:guid}/referenced-by")]
[MapToApiVersion("1.0")]
[ProducesResponseType(typeof(PagedViewModel<IReferenceResponseModel>), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
public async Task<IActionResult> ReferencedBy2(
CancellationToken cancellationToken,
Guid id,
int skip = 0,
int take = 20)
{
Attempt<PagedModel<RelationItemModel>, GetReferencesOperationStatus> relationItemsAttempt = await _trackedReferencesService.GetPagedRelationsForItemAsync(id, UmbracoObjectTypes.Media, skip, take, true);
if (relationItemsAttempt.Success is false)
{
return GetReferencesOperationStatusResult(relationItemsAttempt.Status);
}
var pagedViewModel = new PagedViewModel<IReferenceResponseModel>
{
Total = relationItemsAttempt.Result.Total,
Items = await _relationTypePresentationFactory.CreateReferenceResponseModelsAsync(relationItemsAttempt.Result.Items),
};
return Ok(pagedViewModel);
}
}
@@ -3,11 +3,9 @@ using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Api.Common.ViewModels.Pagination;
using Umbraco.Cms.Api.Management.ViewModels;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Mapping;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Services.OperationStatus;
namespace Umbraco.Cms.Api.Management.Controllers.Media.References;
@@ -16,32 +14,13 @@ public class ReferencedDescendantsMediaController : MediaControllerBase
{
private readonly ITrackedReferencesService _trackedReferencesSkipTakeService;
private readonly IUmbracoMapper _umbracoMapper;
public ReferencedDescendantsMediaController(
ITrackedReferencesService trackedReferencesSkipTakeService,
IUmbracoMapper umbracoMapper)
public ReferencedDescendantsMediaController(ITrackedReferencesService trackedReferencesSkipTakeService, IUmbracoMapper umbracoMapper)
{
_trackedReferencesSkipTakeService = trackedReferencesSkipTakeService;
_umbracoMapper = umbracoMapper;
}
[Obsolete("Use the ReferencedDescendants2 action method instead. Scheduled for removal in Umbraco 19, when ReferencedDescendants2 will be renamed back to ReferencedDescendants.")]
[NonAction]
public async Task<ActionResult<PagedViewModel<ReferenceByIdModel>>> ReferencedDescendants(
CancellationToken cancellationToken,
Guid id,
int skip = 0,
int take = 20)
{
PagedModel<RelationItemModel> relationItems = await _trackedReferencesSkipTakeService.GetPagedDescendantsInReferencesAsync(id, skip, take, true);
var pagedViewModel = new PagedViewModel<ReferenceByIdModel>
{
Total = relationItems.Total,
Items = _umbracoMapper.MapEnumerable<RelationItemModel, ReferenceByIdModel>(relationItems.Items),
};
return pagedViewModel;
}
/// <summary>
/// Gets a page list of the child nodes of the current item used in any kind of relation.
/// </summary>
@@ -53,26 +32,19 @@ public class ReferencedDescendantsMediaController : MediaControllerBase
[HttpGet("{id:guid}/referenced-descendants")]
[MapToApiVersion("1.0")]
[ProducesResponseType(typeof(PagedViewModel<ReferenceByIdModel>), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
public async Task<IActionResult> ReferencedDescendants2(
public async Task<ActionResult<PagedViewModel<ReferenceByIdModel>>> ReferencedDescendants(
CancellationToken cancellationToken,
Guid id,
int skip = 0,
int take = 20)
{
Attempt<PagedModel<RelationItemModel>, GetReferencesOperationStatus> relationItemsAttempt = await _trackedReferencesSkipTakeService.GetPagedDescendantsInReferencesAsync(id, UmbracoObjectTypes.Media, skip, take, true);
if (relationItemsAttempt.Success is false)
{
return GetReferencesOperationStatusResult(relationItemsAttempt.Status);
}
PagedModel<RelationItemModel> relationItems = await _trackedReferencesSkipTakeService.GetPagedDescendantsInReferencesAsync(id, skip, take, true);
var pagedViewModel = new PagedViewModel<ReferenceByIdModel>
{
Total = relationItemsAttempt.Result.Total,
Items = _umbracoMapper.MapEnumerable<RelationItemModel, ReferenceByIdModel>(relationItemsAttempt.Result.Items),
Total = relationItems.Total,
Items = _umbracoMapper.MapEnumerable<RelationItemModel, ReferenceByIdModel>(relationItems.Items),
};
return Ok(pagedViewModel);
return pagedViewModel;
}
}
@@ -24,15 +24,15 @@ public class SearchMemberItemController : MemberItemControllerBase
[HttpGet("search")]
[MapToApiVersion("1.0")]
[ProducesResponseType(typeof(PagedModel<MemberItemResponseModel>), StatusCodes.Status200OK)]
public async Task<IActionResult> SearchWithAllowedTypes(CancellationToken cancellationToken, string query, int skip = 0, int take = 100, [FromQuery]IEnumerable<Guid>? allowedMemberTypes = null)
public Task<IActionResult> SearchWithAllowedTypes(CancellationToken cancellationToken, string query, int skip = 0, int take = 100, [FromQuery]IEnumerable<Guid>? allowedMemberTypes = null)
{
PagedModel<IEntitySlim> searchResult = await _indexedEntitySearchService.SearchAsync(UmbracoObjectTypes.Member, query, null, allowedMemberTypes, false, "*", skip, take);
PagedModel<IEntitySlim> searchResult = _indexedEntitySearchService.Search(UmbracoObjectTypes.Member, query, null, allowedMemberTypes, skip, take);
var result = new PagedModel<MemberItemResponseModel>
{
Items = searchResult.Items.OfType<IMemberEntitySlim>().Select(_memberPresentationFactory.CreateItemResponseModel),
Total = searchResult.Total,
Total = searchResult.Total
};
return await Task.FromResult<IActionResult>(Ok(result));
return Task.FromResult<IActionResult>(Ok(result));
}
}
@@ -4,10 +4,8 @@ using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Api.Common.ViewModels.Pagination;
using Umbraco.Cms.Api.Management.Factories;
using Umbraco.Cms.Api.Management.ViewModels.TrackedReferences;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Services.OperationStatus;
namespace Umbraco.Cms.Api.Management.Controllers.Member.References;
@@ -17,16 +15,22 @@ public class ReferencedByMemberController : MemberControllerBase
private readonly ITrackedReferencesService _trackedReferencesService;
private readonly IRelationTypePresentationFactory _relationTypePresentationFactory;
public ReferencedByMemberController(
ITrackedReferencesService trackedReferencesService,
IRelationTypePresentationFactory relationTypePresentationFactory)
public ReferencedByMemberController(ITrackedReferencesService trackedReferencesService, IRelationTypePresentationFactory relationTypePresentationFactory)
{
_trackedReferencesService = trackedReferencesService;
_relationTypePresentationFactory = relationTypePresentationFactory;
}
[Obsolete("Use the ReferencedBy2 action method instead. Scheduled for removal in Umbraco 19, when ReferencedBy2 will be renamed back to ReferencedBy.")]
[NonAction]
/// <summary>
/// Gets a page list of tracked references for the current item, so you can see where an item is being used.
/// </summary>
/// <remarks>
/// Used by info tabs on content, media etc. and for the delete and unpublish of single items.
/// This is basically finding parents of relations.
/// </remarks>
[HttpGet("{id:guid}/referenced-by")]
[MapToApiVersion("1.0")]
[ProducesResponseType(typeof(PagedViewModel<IReferenceResponseModel>), StatusCodes.Status200OK)]
public async Task<ActionResult<PagedViewModel<IReferenceResponseModel>>> ReferencedBy(
CancellationToken cancellationToken,
Guid id,
@@ -43,37 +47,4 @@ public class ReferencedByMemberController : MemberControllerBase
return pagedViewModel;
}
/// <summary>
/// Gets a page list of tracked references for the current item, so you can see where an item is being used.
/// </summary>
/// <remarks>
/// Used by info tabs on content, media etc. and for the delete and unpublish of single items.
/// This is basically finding parents of relations.
/// </remarks>
[HttpGet("{id:guid}/referenced-by")]
[MapToApiVersion("1.0")]
[ProducesResponseType(typeof(PagedViewModel<IReferenceResponseModel>), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
public async Task<IActionResult> ReferencedBy2(
CancellationToken cancellationToken,
Guid id,
int skip = 0,
int take = 20)
{
Attempt<PagedModel<RelationItemModel>, GetReferencesOperationStatus> relationItemsAttempt = await _trackedReferencesService.GetPagedRelationsForItemAsync(id, UmbracoObjectTypes.Member, skip, take, true);
if (relationItemsAttempt.Success is false)
{
return GetReferencesOperationStatusResult(relationItemsAttempt.Status);
}
var pagedViewModel = new PagedViewModel<IReferenceResponseModel>
{
Total = relationItemsAttempt.Result.Total,
Items = await _relationTypePresentationFactory.CreateReferenceResponseModelsAsync(relationItemsAttempt.Result.Items),
};
return Ok(pagedViewModel);
}
}
@@ -3,11 +3,9 @@ using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Api.Common.ViewModels.Pagination;
using Umbraco.Cms.Api.Management.ViewModels;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Mapping;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Services.OperationStatus;
namespace Umbraco.Cms.Api.Management.Controllers.Member.References;
@@ -17,16 +15,23 @@ public class ReferencedDescendantsMemberController : MemberControllerBase
private readonly ITrackedReferencesService _trackedReferencesSkipTakeService;
private readonly IUmbracoMapper _umbracoMapper;
public ReferencedDescendantsMemberController(
ITrackedReferencesService trackedReferencesSkipTakeService,
IUmbracoMapper umbracoMapper)
public ReferencedDescendantsMemberController(ITrackedReferencesService trackedReferencesSkipTakeService, IUmbracoMapper umbracoMapper)
{
_trackedReferencesSkipTakeService = trackedReferencesSkipTakeService;
_umbracoMapper = umbracoMapper;
}
[Obsolete("Use the ReferencedDescendants2 action method instead. Scheduled for removal in Umbraco 19, when ReferencedDescendants2 will be renamed back to ReferencedDescendants.")]
[NonAction]
/// <summary>
/// Gets a page list of the child nodes of the current item used in any kind of relation.
/// </summary>
/// <remarks>
/// Used when deleting and unpublishing a single item to check if this item has any descending items that are in any
/// kind of relation.
/// This is basically finding the descending items which are children in relations.
/// </remarks>
[HttpGet("{id:guid}/referenced-descendants")]
[MapToApiVersion("1.0")]
[ProducesResponseType(typeof(PagedViewModel<ReferenceByIdModel>), StatusCodes.Status200OK)]
public async Task<ActionResult<PagedViewModel<ReferenceByIdModel>>> ReferencedDescendants(
CancellationToken cancellationToken,
Guid id,
@@ -42,38 +47,4 @@ public class ReferencedDescendantsMemberController : MemberControllerBase
return pagedViewModel;
}
/// <summary>
/// Gets a page list of the child nodes of the current item used in any kind of relation.
/// </summary>
/// <remarks>
/// Used when deleting and unpublishing a single item to check if this item has any descending items that are in any
/// kind of relation.
/// This is basically finding the descending items which are children in relations.
/// </remarks>
[HttpGet("{id:guid}/referenced-descendants")]
[MapToApiVersion("1.0")]
[ProducesResponseType(typeof(PagedViewModel<ReferenceByIdModel>), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
public async Task<IActionResult> ReferencedDescendants2(
CancellationToken cancellationToken,
Guid id,
int skip = 0,
int take = 20)
{
Attempt<PagedModel<RelationItemModel>, GetReferencesOperationStatus> relationItemsAttempt = await _trackedReferencesSkipTakeService.GetPagedDescendantsInReferencesAsync(id, UmbracoObjectTypes.Member, skip, take, true);
if (relationItemsAttempt.Success is false)
{
return GetReferencesOperationStatusResult(relationItemsAttempt.Status);
}
var pagedViewModel = new PagedViewModel<ReferenceByIdModel>
{
Total = relationItemsAttempt.Result.Total,
Items = _umbracoMapper.MapEnumerable<RelationItemModel, ReferenceByIdModel>(relationItemsAttempt.Result.Items),
};
return Ok(pagedViewModel);
}
}
@@ -1,25 +0,0 @@
using Asp.Versioning;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Api.Management.ViewModels.Folder;
using Umbraco.Cms.Core.Security;
using Umbraco.Cms.Core.Services;
namespace Umbraco.Cms.Api.Management.Controllers.MemberType.Folder;
[ApiVersion("1.0")]
public class ByKeyMemberTypeFolderController : MemberTypeFolderControllerBase
{
public ByKeyMemberTypeFolderController(
IBackOfficeSecurityAccessor backOfficeSecurityAccessor,
IMemberTypeContainerService memberTypeContainerService)
: base(backOfficeSecurityAccessor, memberTypeContainerService)
{
}
[HttpGet("{id:guid}")]
[MapToApiVersion("1.0")]
[ProducesResponseType(typeof(FolderResponseModel), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
public async Task<IActionResult> ByKey(CancellationToken cancellationToken, Guid id) => await GetFolderAsync(id);
}
@@ -1,31 +0,0 @@
using Asp.Versioning;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Api.Management.ViewModels.Folder;
using Umbraco.Cms.Core.Security;
using Umbraco.Cms.Core.Services;
namespace Umbraco.Cms.Api.Management.Controllers.MemberType.Folder;
[ApiVersion("1.0")]
public class CreateMemberTypeFolderController : MemberTypeFolderControllerBase
{
public CreateMemberTypeFolderController(
IBackOfficeSecurityAccessor backOfficeSecurityAccessor,
IMemberTypeContainerService memberTypeContainerService)
: base(backOfficeSecurityAccessor, memberTypeContainerService)
{
}
[HttpPost]
[MapToApiVersion("1.0")]
[ProducesResponseType(StatusCodes.Status201Created)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
public async Task<IActionResult> Create(
CancellationToken cancellationToken,
CreateFolderRequestModel createFolderRequestModel)
=> await CreateFolderAsync<ByKeyMemberTypeFolderController>(
createFolderRequestModel,
controller => nameof(controller.ByKey)).ConfigureAwait(false);
}
@@ -1,25 +0,0 @@
using Asp.Versioning;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Core.Security;
using Umbraco.Cms.Core.Services;
namespace Umbraco.Cms.Api.Management.Controllers.MemberType.Folder;
[ApiVersion("1.0")]
public class DeleteMemberTypeFolderController : MemberTypeFolderControllerBase
{
public DeleteMemberTypeFolderController(
IBackOfficeSecurityAccessor backOfficeSecurityAccessor,
IMemberTypeContainerService memberTypeContainerService)
: base(backOfficeSecurityAccessor, memberTypeContainerService)
{
}
[HttpDelete("{id:guid}")]
[MapToApiVersion("1.0")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
public async Task<IActionResult> Delete(CancellationToken cancellationToken, Guid id) => await DeleteFolderAsync(id);
}
@@ -1,23 +0,0 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Api.Management.Routing;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Security;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Web.Common.Authorization;
namespace Umbraco.Cms.Api.Management.Controllers.MemberType.Folder;
[VersionedApiBackOfficeRoute($"{Constants.UdiEntityType.MemberType}/folder")]
[ApiExplorerSettings(GroupName = "Member Type")]
[Authorize(Policy = AuthorizationPolicies.TreeAccessMemberTypes)]
public abstract class MemberTypeFolderControllerBase : FolderManagementControllerBase<IMemberType>
{
protected MemberTypeFolderControllerBase(
IBackOfficeSecurityAccessor backOfficeSecurityAccessor,
IMemberTypeContainerService memberTypeContainerService)
: base(backOfficeSecurityAccessor, memberTypeContainerService)
{
}
}
@@ -1,30 +0,0 @@
using Asp.Versioning;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Api.Management.ViewModels.Folder;
using Umbraco.Cms.Core.Security;
using Umbraco.Cms.Core.Services;
namespace Umbraco.Cms.Api.Management.Controllers.MemberType.Folder;
[ApiVersion("1.0")]
public class UpdateMemberTypeFolderController : MemberTypeFolderControllerBase
{
public UpdateMemberTypeFolderController(
IBackOfficeSecurityAccessor backOfficeSecurityAccessor,
IMemberTypeContainerService memberTypeContainerService)
: base(backOfficeSecurityAccessor, memberTypeContainerService)
{
}
[HttpPut("{id:guid}")]
[MapToApiVersion("1.0")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
public async Task<IActionResult> Update(
CancellationToken cancellationToken,
Guid id,
UpdateFolderResponseModel updateFolderResponseModel)
=> await UpdateFolderAsync(id, updateFolderResponseModel);
}
@@ -30,7 +30,6 @@ public class RootMemberTypeTreeController : MemberTypeTreeControllerBase
public async Task<ActionResult<PagedViewModel<MemberTypeTreeItemResponseModel>>> Root(
CancellationToken cancellationToken,
int skip = 0,
int take = 100,
bool foldersOnly = false)
int take = 100)
=> await GetRoot(skip, take);
}
@@ -1,4 +1,4 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Api.Common.ViewModels.Pagination;
using Umbraco.Cms.Api.Management.Services.Flags;
@@ -23,7 +23,6 @@ public class SiblingMemberTypeTreeController : MemberTypeTreeControllerBase
CancellationToken cancellationToken,
Guid target,
int before,
int after,
bool foldersOnly = false)
int after)
=> await GetSiblings(target, before, after);
}
@@ -1,18 +1,21 @@
using System;
using System.Threading.Tasks;
using Asp.Versioning;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Configuration.Models;
using Umbraco.Cms.Infrastructure.ModelsBuilder;
using Umbraco.Cms.Infrastructure.ModelsBuilder.Building;
using Umbraco.Extensions;
namespace Umbraco.Cms.Api.Management.Controllers.ModelsBuilder;
[ApiVersion("1.0")]
public class BuildModelsBuilderController : ModelsBuilderControllerBase
{
private readonly ModelsBuilderSettings _modelsBuilderSettings;
private ModelsBuilderSettings _modelsBuilderSettings;
private readonly ModelsGenerationError _mbErrors;
private readonly IModelsGenerator _modelGenerator;
@@ -24,6 +27,8 @@ public class BuildModelsBuilderController : ModelsBuilderControllerBase
_mbErrors = mbErrors;
_modelGenerator = modelGenerator;
_modelsBuilderSettings = modelsBuilderSettings.CurrentValue;
modelsBuilderSettings.OnChange(x => _modelsBuilderSettings = x);
}
[HttpPost("build")]
@@ -34,8 +39,7 @@ public class BuildModelsBuilderController : ModelsBuilderControllerBase
{
try
{
if (_modelsBuilderSettings.ModelsMode != Constants.ModelsBuilder.ModelsModes.SourceCodeManual
&& _modelsBuilderSettings.ModelsMode != Constants.ModelsBuilder.ModelsModes.SourceCodeAuto)
if (!_modelsBuilderSettings.ModelsMode.SupportsExplicitGeneration())
{
var problemDetailsModel = new ProblemDetails
{
@@ -1,22 +0,0 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Api.Management.Services.NewsDashboard;
using Umbraco.Cms.Api.Management.ViewModels.NewsDashboard;
namespace Umbraco.Cms.Api.Management.Controllers.NewsDashboard;
public class NewsDashboardController : NewsDashboardControllerBase
{
private readonly INewsDashboardService _newsDashboardService;
public NewsDashboardController(INewsDashboardService newsDashboardService) => _newsDashboardService = newsDashboardService;
[HttpGet]
[ProducesResponseType(typeof(NewsDashboardResponseModel), StatusCodes.Status200OK)]
public async Task<IActionResult> GetDashboard()
{
NewsDashboardResponseModel content = await _newsDashboardService.GetItemsAsync();
return Ok(content);
}
}
@@ -1,10 +0,0 @@
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Api.Management.Routing;
namespace Umbraco.Cms.Api.Management.Controllers.NewsDashboard;
[VersionedApiBackOfficeRoute("news-dashboard")]
[ApiExplorerSettings(GroupName = "News Dashboard")]
public abstract class NewsDashboardControllerBase : ManagementApiControllerBase
{
}
@@ -7,7 +7,6 @@ using Umbraco.Cms.Core.Services;
namespace Umbraco.Cms.Api.Management.Controllers.Preview;
[ApiVersion("1.0")]
[Obsolete("Do not use this. Preview state is initiated implicitly by the preview URL generation. Scheduled for removal in V18.")]
public class EnterPreviewController : PreviewControllerBase
{
private readonly IPreviewService _previewService;
@@ -254,7 +254,7 @@ public class BackOfficeController : SecurityControllerBase
if (associatedUser is not null)
{
// log current datetime as last login (this also ensures that the user is not flagged as inactive)
associatedUser.LastLoginDate = DateTime.UtcNow;
associatedUser.LastLoginDateUtc = DateTime.UtcNow;
await _backOfficeUserManager.UpdateAsync(associatedUser);
return await SignInBackOfficeUser(associatedUser, request);
@@ -17,6 +17,12 @@ public class BackOfficeDefaultController : Controller
public BackOfficeDefaultController(IRuntime umbracoRuntime)
=> _umbracoRuntime = umbracoRuntime;
[Obsolete("Use the non obsoleted constructor instead. Scheduled to be removed in v17")]
public BackOfficeDefaultController()
: this(StaticServiceProvider.Instance.GetRequiredService<IRuntime>())
{
}
[HttpGet]
[AllowAnonymous]
public async Task<IActionResult> Index(CancellationToken cancellationToken)
@@ -34,14 +34,14 @@ public class StaticFileTreeControllerBase : FileSystemTreeControllerBase
protected override IFileSystem FileSystem { get; }
protected override string[] GetDirectories(string path) =>
protected string[] GetDirectories(string path) =>
IsTreeRootPath(path)
? _allowedRootFolders
: IsAllowedPath(path)
? _fileSystemTreeService.GetDirectories(path)
: Array.Empty<string>();
protected override string[] GetFiles(string path)
protected string[] GetFiles(string path)
=> IsTreeRootPath(path) || IsAllowedPath(path) == false
? Array.Empty<string>()
: _fileSystemTreeService.GetFiles(path);
@@ -43,6 +43,41 @@ public class ExecuteTemplateQueryController : TemplateQueryControllerBase
_publishedContentStatusFilteringService = publishedContentStatusFilteringService;
}
[Obsolete("Please use the non-obsolete constructor. Will be removed in V17.")]
public ExecuteTemplateQueryController(
IPublishedContentQuery publishedContentQuery,
IVariationContextAccessor variationContextAccessor,
IPublishedValueFallback publishedValueFallback,
IContentTypeService contentTypeService,
IPublishedContentCache contentCache,
IDocumentNavigationQueryService documentNavigationQueryService,
IPublishedContentStatusFilteringService publishedContentStatusFilteringService)
: this(
publishedContentQuery,
publishedValueFallback,
contentTypeService,
documentNavigationQueryService,
publishedContentStatusFilteringService)
{
}
[Obsolete("Please use the non-obsolete constructor. Will be removed in V17.")]
public ExecuteTemplateQueryController(
IPublishedContentQuery publishedContentQuery,
IVariationContextAccessor variationContextAccessor,
IPublishedValueFallback publishedValueFallback,
IContentTypeService contentTypeService,
IPublishedContentCache contentCache,
IDocumentNavigationQueryService documentNavigationQueryService)
: this(
publishedContentQuery,
publishedValueFallback,
contentTypeService,
documentNavigationQueryService,
StaticServiceProvider.Instance.GetRequiredService<IPublishedContentStatusFilteringService>())
{
}
[HttpPost("execute")]
[MapToApiVersion("1.0")]
[ProducesResponseType(typeof(TemplateQueryResultResponseModel), StatusCodes.Status200OK)]
@@ -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()
@@ -11,13 +11,13 @@ internal static class AuditLogBuilderExtensions
internal static IUmbracoBuilder AddAuditLogs(this IUmbracoBuilder builder)
{
builder.Services.AddTransient<IAuditLogPresentationFactory, AuditLogPresentationFactory>();
builder.AddNotificationAsyncHandler<UserLoginSuccessNotification, BackOfficeUserManagerAuditer>();
builder.AddNotificationAsyncHandler<UserLogoutSuccessNotification, BackOfficeUserManagerAuditer>();
builder.AddNotificationAsyncHandler<UserLoginFailedNotification, BackOfficeUserManagerAuditer>();
builder.AddNotificationAsyncHandler<UserForgotPasswordRequestedNotification, BackOfficeUserManagerAuditer>();
builder.AddNotificationAsyncHandler<UserForgotPasswordChangedNotification, BackOfficeUserManagerAuditer>();
builder.AddNotificationAsyncHandler<UserPasswordChangedNotification, BackOfficeUserManagerAuditer>();
builder.AddNotificationAsyncHandler<UserPasswordResetNotification, BackOfficeUserManagerAuditer>();
builder.AddNotificationHandler<UserLoginSuccessNotification, BackOfficeUserManagerAuditer>();
builder.AddNotificationHandler<UserLogoutSuccessNotification, BackOfficeUserManagerAuditer>();
builder.AddNotificationHandler<UserLoginFailedNotification, BackOfficeUserManagerAuditer>();
builder.AddNotificationHandler<UserForgotPasswordRequestedNotification, BackOfficeUserManagerAuditer>();
builder.AddNotificationHandler<UserForgotPasswordChangedNotification, BackOfficeUserManagerAuditer>();
builder.AddNotificationHandler<UserPasswordChangedNotification, BackOfficeUserManagerAuditer>();
builder.AddNotificationHandler<UserPasswordResetNotification, BackOfficeUserManagerAuditer>();
return builder;
}
@@ -1,15 +0,0 @@
using Microsoft.Extensions.DependencyInjection;
using Umbraco.Cms.Api.Management.Services.NewsDashboard;
using Umbraco.Cms.Core.DependencyInjection;
namespace Umbraco.Cms.Api.Management.DependencyInjection;
internal static class NewsDashboardBuilderExtensions
{
internal static IUmbracoBuilder AddNewsDashboard(this IUmbracoBuilder builder)
{
builder.Services.AddSingleton<INewsDashboardService, NewsDashboardService>();
return builder;
}
}
@@ -25,7 +25,7 @@ public static class InstallerBuilderExtensions
builder.AddInstallSteps();
services.AddTransient<IInstallService, InstallService>();
builder.AddNotificationAsyncHandler<UnattendedInstallNotification, PostUnattendedInstallNotificationHandler>();
builder.AddNotificationAsyncHandler<UnattendedInstallNotification, CreateUnattendedUserNotificationHandler>();
builder.WithCollectionBuilder<MapDefinitionCollectionBuilder>().Add<InstallerViewModelsMapDefinition>();
return builder;
@@ -30,7 +30,7 @@ public static partial class UmbracoBuilderExtensions
.AddMembersIdentity()
.AddUmbracoProfiler()
.AddMvcAndRazor(configureMvc)
.AddBackgroundJobs()
.AddRecurringBackgroundJobs()
.AddUmbracoHybridCache()
.AddDistributedCache()
.AddCoreNotifications();
@@ -10,26 +10,18 @@ namespace Umbraco.Extensions
{
internal static void AddCollectionBuilders(this IUmbracoBuilder builder)
{
builder.FlagProviders()
builder.SignProviders()
.Append<HasScheduleFlagProvider>()
.Append<IsProtectedFlagProvider>()
.Append<HasPendingChangesFlagProvider>()
.Append<HasCollectionFlagProvider>();
}
/// <summary>
/// Gets the flag providers collection builder.
/// </summary>
/// <param name="builder">The builder.</param>
public static FlagProviderCollectionBuilder FlagProviders(this IUmbracoBuilder builder)
=> builder.WithCollectionBuilder<FlagProviderCollectionBuilder>();
/// <summary>
/// Gets the sign providers collection builder.
/// </summary>
/// <param name="builder">The builder.</param>
[Obsolete("Please use the correctly named FlagProviders. Scheduled for removal in Umbraco 19.")]
public static FlagProviderCollectionBuilder SignProviders(this IUmbracoBuilder builder)
=> builder.FlagProviders();
=> builder.WithCollectionBuilder<FlagProviderCollectionBuilder>();
}
}
@@ -71,8 +71,7 @@ public static partial class UmbracoBuilderExtensions
.AddUserData()
.AddSegment()
.AddExport()
.AddImport()
.AddNewsDashboard();
.AddImport();
services
.ConfigureOptions<ConfigureApiBehaviorOptions>()
@@ -14,6 +14,15 @@ public class DocumentCollectionPresentationFactory : ContentCollectionPresentati
private readonly IPublicAccessService _publicAccessService;
private readonly IEntityService _entityService;
[Obsolete("Please use the non-obsolete constructor. Scheduled for removal in V17.")]
public DocumentCollectionPresentationFactory(IUmbracoMapper mapper, IPublicAccessService publicAccessService)
: this(
mapper,
publicAccessService,
StaticServiceProvider.Instance.GetRequiredService<IEntityService>())
{
}
[ActivatorUtilitiesConstructor]
public DocumentCollectionPresentationFactory(IUmbracoMapper mapper, IPublicAccessService publicAccessService, IEntityService entityService)
: base(mapper)
@@ -65,6 +65,22 @@ internal sealed class DocumentPresentationFactory : IDocumentPresentationFactory
_flagProviderCollection = flagProviderCollection;
}
[Obsolete("Schedule for removal in v17")]
public async Task<DocumentResponseModel> CreateResponseModelAsync(IContent content)
{
DocumentResponseModel responseModel = _umbracoMapper.Map<DocumentResponseModel>(content)!;
Guid? templateKey = content.TemplateId.HasValue
? _templateService.GetAsync(content.TemplateId.Value).Result?.Key
: null;
responseModel.Template = templateKey.HasValue
? new ReferenceByIdModel { Id = templateKey.Value }
: null;
return responseModel;
}
public async Task<PublishedDocumentResponseModel> CreatePublishedResponseModelAsync(IContent content)
{
PublishedDocumentResponseModel responseModel = _umbracoMapper.Map<PublishedDocumentResponseModel>(content)!;
@@ -165,6 +181,68 @@ internal sealed class DocumentPresentationFactory : IDocumentPresentationFactory
public DocumentTypeReferenceResponseModel CreateDocumentTypeReferenceResponseModel(IDocumentEntitySlim entity)
=> _umbracoMapper.Map<DocumentTypeReferenceResponseModel>(entity)!;
[Obsolete("Use CreateCulturePublishScheduleModels instead. Scheduled for removal in v17")]
public Attempt<CultureAndScheduleModel, ContentPublishingOperationStatus> CreateCultureAndScheduleModel(PublishDocumentRequestModel requestModel)
{
var contentScheduleCollection = new ContentScheduleCollection();
var culturesToPublishImmediately = new HashSet<string>();
foreach (CultureAndScheduleRequestModel cultureAndScheduleRequestModel in requestModel.PublishSchedules)
{
if (cultureAndScheduleRequestModel.Schedule is null || (cultureAndScheduleRequestModel.Schedule.PublishTime is null && cultureAndScheduleRequestModel.Schedule.UnpublishTime is null))
{
culturesToPublishImmediately.Add(cultureAndScheduleRequestModel.Culture ?? Constants.System.InvariantCulture); // API have `null` for invariant, but service layer has "*".
continue;
}
if (cultureAndScheduleRequestModel.Schedule.PublishTime is not null)
{
if (cultureAndScheduleRequestModel.Schedule.PublishTime <= _timeProvider.GetUtcNow())
{
return Attempt.FailWithStatus(ContentPublishingOperationStatus.PublishTimeNeedsToBeInFuture, new CultureAndScheduleModel()
{
Schedules = contentScheduleCollection,
CulturesToPublishImmediately = culturesToPublishImmediately,
});
}
contentScheduleCollection.Add(new ContentSchedule(
cultureAndScheduleRequestModel.Culture ?? Constants.System.InvariantCulture,
cultureAndScheduleRequestModel.Schedule.PublishTime.Value.UtcDateTime,
ContentScheduleAction.Release));
}
if (cultureAndScheduleRequestModel.Schedule.UnpublishTime is not null)
{
if (cultureAndScheduleRequestModel.Schedule.UnpublishTime <= cultureAndScheduleRequestModel.Schedule.PublishTime)
{
return Attempt.FailWithStatus(ContentPublishingOperationStatus.UnpublishTimeNeedsToBeAfterPublishTime, new CultureAndScheduleModel()
{
Schedules = contentScheduleCollection,
CulturesToPublishImmediately = culturesToPublishImmediately,
});
}
if (cultureAndScheduleRequestModel.Schedule.UnpublishTime <= _timeProvider.GetUtcNow())
{
return Attempt.FailWithStatus(ContentPublishingOperationStatus.UpublishTimeNeedsToBeInFuture, new CultureAndScheduleModel()
{
Schedules = contentScheduleCollection,
CulturesToPublishImmediately = culturesToPublishImmediately,
});
}
contentScheduleCollection.Add(new ContentSchedule(
cultureAndScheduleRequestModel.Culture ?? Constants.System.InvariantCulture,
cultureAndScheduleRequestModel.Schedule.UnpublishTime.Value.UtcDateTime,
ContentScheduleAction.Expire));
}
}
return Attempt.SucceedWithStatus(ContentPublishingOperationStatus.Success, new CultureAndScheduleModel()
{
Schedules = contentScheduleCollection,
CulturesToPublishImmediately = culturesToPublishImmediately,
});
}
public Attempt<List<CulturePublishScheduleModel>, ContentPublishingOperationStatus> CreateCulturePublishScheduleModels(PublishDocumentRequestModel requestModel)
{
var model = new List<CulturePublishScheduleModel>();
@@ -1,42 +1,24 @@
using Microsoft.Extensions.Logging;
using Umbraco.Cms.Api.Management.Routing;
using Umbraco.Cms.Api.Management.ViewModels.Document;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.Membership;
using Umbraco.Cms.Core.Routing;
using Umbraco.Cms.Core.Security;
using Umbraco.Cms.Core.Services;
using Umbraco.Extensions;
namespace Umbraco.Cms.Api.Management.Factories;
public class DocumentUrlFactory : IDocumentUrlFactory
{
private readonly IPublishedUrlInfoProvider _publishedUrlInfoProvider;
private readonly UrlProviderCollection _urlProviders;
private readonly IPreviewService _previewService;
private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor;
private readonly ILogger<DocumentUrlFactory> _logger;
public DocumentUrlFactory(
IPublishedUrlInfoProvider publishedUrlInfoProvider,
UrlProviderCollection urlProviders,
IPreviewService previewService,
IBackOfficeSecurityAccessor backOfficeSecurityAccessor,
ILogger<DocumentUrlFactory> logger)
{
_publishedUrlInfoProvider = publishedUrlInfoProvider;
_urlProviders = urlProviders;
_previewService = previewService;
_backOfficeSecurityAccessor = backOfficeSecurityAccessor;
_logger = logger;
}
public DocumentUrlFactory(IPublishedUrlInfoProvider publishedUrlInfoProvider)
=> _publishedUrlInfoProvider = publishedUrlInfoProvider;
public async Task<IEnumerable<DocumentUrlInfo>> CreateUrlsAsync(IContent content)
{
ISet<UrlInfo> urlInfos = await _publishedUrlInfoProvider.GetAllAsync(content);
return urlInfos
.Select(CreateDocumentUrlInfo)
.Where(urlInfo => urlInfo.IsUrl)
.Select(urlInfo => new DocumentUrlInfo { Culture = urlInfo.Culture, Url = urlInfo.Text })
.ToArray();
}
@@ -52,52 +34,4 @@ public class DocumentUrlFactory : IDocumentUrlFactory
return documentUrlInfoResourceSets;
}
public async Task<DocumentUrlInfo?> GetPreviewUrlAsync(IContent content, string providerAlias, string? culture, string? segment)
{
IUrlProvider? provider = _urlProviders.FirstOrDefault(provider => provider.Alias.InvariantEquals(providerAlias));
if (provider is null)
{
_logger.LogError("Could not resolve a URL provider requested for preview - it was not registered in the URL providers collection.");
return null;
}
UrlInfo? previewUrlInfo = await provider.GetPreviewUrlAsync(content, culture, segment);
if (previewUrlInfo is null)
{
_logger.LogError("The URL provider could not generate a preview URL for content with key: {contentKey}", content.Key);
return null;
}
// must initiate preview state for internal preview URLs
if (previewUrlInfo.Url is not null && previewUrlInfo.IsExternal is false)
{
IUser? currentUser = _backOfficeSecurityAccessor.BackOfficeSecurity?.CurrentUser;
if (currentUser is null)
{
_logger.LogError("Could not access the current backoffice user while attempting to authenticate for preview.");
return null;
}
if (await _previewService.TryEnterPreviewAsync(currentUser) is false)
{
_logger.LogError("A server error occured, could not initiate an authenticated preview state for the current user.");
return null;
}
}
return CreateDocumentUrlInfo(previewUrlInfo);
}
private DocumentUrlInfo CreateDocumentUrlInfo(UrlInfo urlInfo)
{
var url = urlInfo.Url?.ToString();
return new DocumentUrlInfo
{
Culture = urlInfo.Culture,
Url = url,
Message = urlInfo.Message,
Provider = urlInfo.Provider,
};
}
}
@@ -41,6 +41,14 @@ public class HealthCheckGroupPresentationFactory : IHealthCheckGroupPresentation
return groups;
}
[Obsolete("Use CreateHealthCheckGroupWithResultViewModelAsync instead. Will be removed in v17.")]
public HealthCheckGroupWithResultResponseModel CreateHealthCheckGroupWithResultViewModel(IGrouping<string?, HealthCheck> healthCheckGroup)
=> CreateHealthCheckGroupWithResultViewModelAsync(healthCheckGroup).GetAwaiter().GetResult();
[Obsolete("Use CreateHealthCheckGroupWithResultViewModelAsync instead. Will be removed in v17.")]
public HealthCheckWithResultPresentationModel CreateHealthCheckWithResultViewModel(HealthCheck healthCheck) =>
CreateHealthCheckWithResultViewModelAsync(healthCheck).GetAwaiter().GetResult();
public async Task<HealthCheckGroupWithResultResponseModel> CreateHealthCheckGroupWithResultViewModelAsync(IGrouping<string?, HealthCheck> healthCheckGroup)
{
var healthChecks = new List<HealthCheckWithResultPresentationModel>();
@@ -14,9 +14,17 @@ namespace Umbraco.Cms.Api.Management.Factories;
public interface IDocumentPresentationFactory
{
[Obsolete("Schedule for removal in v17")]
Task<DocumentResponseModel> CreateResponseModelAsync(IContent content);
Task<PublishedDocumentResponseModel> CreatePublishedResponseModelAsync(IContent content);
Task<DocumentResponseModel> CreateResponseModelAsync(IContent content, ContentScheduleCollection schedule);
Task<DocumentResponseModel> CreateResponseModelAsync(IContent content, ContentScheduleCollection schedule)
#pragma warning disable CS0618 // Type or member is obsolete
// Remove when obsolete CreateResponseModelAsync is removed
=> CreateResponseModelAsync(content);
#pragma warning restore CS0618 // Type or member is obsolete
DocumentItemResponseModel CreateItemResponseModel(IDocumentEntitySlim entity);
DocumentBlueprintItemResponseModel CreateBlueprintItemResponseModel(IDocumentEntitySlim entity);
@@ -25,6 +33,9 @@ public interface IDocumentPresentationFactory
DocumentTypeReferenceResponseModel CreateDocumentTypeReferenceResponseModel(IDocumentEntitySlim entity);
[Obsolete("Use CreateCulturePublishScheduleModels instead. Scheduled for removal in v17")]
Attempt<CultureAndScheduleModel, ContentPublishingOperationStatus> CreateCultureAndScheduleModel(PublishDocumentRequestModel requestModel);
Attempt<List<CulturePublishScheduleModel>, ContentPublishingOperationStatus> CreateCulturePublishScheduleModels(
PublishDocumentRequestModel requestModel)
{
@@ -8,6 +8,4 @@ public interface IDocumentUrlFactory
Task<IEnumerable<DocumentUrlInfo>> CreateUrlsAsync(IContent content);
Task<IEnumerable<DocumentUrlInfoResponseModel>> CreateUrlSetsAsync(IEnumerable<IContent> contentItems);
Task<DocumentUrlInfo?> GetPreviewUrlAsync(IContent content, string providerAlias, string? culture, string? segment);
}
@@ -7,8 +7,19 @@ public interface IHealthCheckGroupPresentationFactory
{
IEnumerable<IGrouping<string?, HealthCheck>> CreateGroupingFromHealthCheckCollection();
Task<HealthCheckGroupWithResultResponseModel> CreateHealthCheckGroupWithResultViewModelAsync(
IGrouping<string?, HealthCheck> healthCheckGroup);
[Obsolete("Use CreateHealthCheckGroupWithResultViewModelAsync instead. Will be removed in v17.")]
HealthCheckGroupWithResultResponseModel CreateHealthCheckGroupWithResultViewModel(IGrouping<string?, HealthCheck> healthCheckGroup);
Task<HealthCheckWithResultPresentationModel> CreateHealthCheckWithResultViewModelAsync(HealthCheck healthCheck);
[Obsolete("Use CreateHealthCheckGroupWithResultViewModelAsync instead. Will be removed in v17.")]
HealthCheckWithResultPresentationModel CreateHealthCheckWithResultViewModel(HealthCheck healthCheck);
Task<HealthCheckGroupWithResultResponseModel> CreateHealthCheckGroupWithResultViewModelAsync(IGrouping<string?, HealthCheck> healthCheckGroup)
#pragma warning disable CS0618 // Type or member is obsolete
=> Task.FromResult(CreateHealthCheckGroupWithResultViewModel(healthCheckGroup));
#pragma warning restore CS0618 // Type or member is obsolete
Task<HealthCheckWithResultPresentationModel> CreateHealthCheckWithResultViewModelAsync(HealthCheck healthCheck)
#pragma warning disable CS0618 // Type or member is obsolete
=> Task.FromResult(CreateHealthCheckWithResultViewModel(healthCheck));
#pragma warning restore CS0618 // Type or member is obsolete
}
@@ -5,8 +5,5 @@ namespace Umbraco.Cms.Api.Management.Factories;
public interface IIndexPresentationFactory
{
[Obsolete("Use CreateAsync() instead. Scheduled for removal in v19.")]
IndexResponseModel Create(IIndex index);
Task<IndexResponseModel> CreateAsync(IIndex index) => Task.FromResult(Create(index));
}
@@ -28,17 +28,21 @@ public class IndexPresentationFactory : IIndexPresentationFactory
_logger = logger;
}
/// <inheritdoc />
[Obsolete("Use CreateAsync() instead. Scheduled for removal in v19.")]
public IndexResponseModel Create(IIndex index)
=> CreateAsync(index).GetAwaiter().GetResult();
[Obsolete("Use the non obsolete method instead. Scheduled for removal in v17")]
public IndexPresentationFactory(IIndexDiagnosticsFactory indexDiagnosticsFactory, IIndexRebuilder indexRebuilder, IIndexingRebuilderService indexingRebuilderService)
:this(
indexDiagnosticsFactory,
indexRebuilder,
indexingRebuilderService,
StaticServiceProvider.Instance.GetRequiredService<ILogger<IndexPresentationFactory>>())
{
}
/// <inheritdoc />
public async Task<IndexResponseModel> CreateAsync(IIndex index)
public IndexResponseModel Create(IIndex index)
{
var isCorrupt = !TryGetSearcherName(index, out var searcherName);
if (await _indexingRebuilderService.IsRebuildingAsync(index.Name))
if (_indexingRebuilderService.IsRebuilding(index.Name))
{
return new IndexResponseModel
{
@@ -59,7 +63,7 @@ public class IndexPresentationFactory : IIndexPresentationFactory
var properties = new Dictionary<string, object?>();
foreach (KeyValuePair<string, object?> property in indexDiag.Metadata)
foreach (var property in indexDiag.Metadata)
{
if (property.Value is null)
{
@@ -67,7 +71,7 @@ public class IndexPresentationFactory : IIndexPresentationFactory
}
else
{
Type propertyType = property.Value.GetType();
var propertyType = property.Value.GetType();
properties[property.Key] = propertyType.IsClass && !propertyType.IsArray ? property.Value?.ToString() : property.Value;
}
}
@@ -1,29 +1,35 @@
using System.Text;
using Microsoft.Extensions.Options;
using Umbraco.Cms.Api.Management.ViewModels.ModelsBuilderDashboard;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Configuration;
using Umbraco.Cms.Core.Configuration.Models;
using Umbraco.Cms.Infrastructure.ModelsBuilder;
using Umbraco.Cms.Api.Management.ViewModels.ModelsBuilderDashboard;
using Umbraco.Extensions;
namespace Umbraco.Cms.Api.Management.Factories;
public class ModelsBuilderPresentationFactory : IModelsBuilderPresentationFactory
{
private ModelsBuilderSettings _modelsBuilderSettings;
private readonly ModelsGenerationError _mbErrors;
private readonly OutOfDateModelsStatus _outOfDateModels;
private readonly ModelsBuilderSettings _modelsBuilderSettings;
public ModelsBuilderPresentationFactory(IOptionsMonitor<ModelsBuilderSettings> modelsBuilderSettings, ModelsGenerationError mbErrors, OutOfDateModelsStatus outOfDateModels)
{
_mbErrors = mbErrors;
_outOfDateModels = outOfDateModels;
_modelsBuilderSettings = modelsBuilderSettings.CurrentValue;
modelsBuilderSettings.OnChange(x => _modelsBuilderSettings = x);
}
public ModelsBuilderResponseModel Create() =>
new()
{
Mode = _modelsBuilderSettings.ModelsMode,
CanGenerate = _modelsBuilderSettings.ModelsMode is Constants.ModelsBuilder.ModelsModes.SourceCodeManual or Constants.ModelsBuilder.ModelsModes.SourceCodeAuto,
CanGenerate = _modelsBuilderSettings.ModelsMode.SupportsExplicitGeneration(),
OutOfDateModels = _outOfDateModels.IsOutOfDate,
LastError = _mbErrors.GetLastError(),
Version = ApiVersion.Current.Version.ToString(),
@@ -18,6 +18,16 @@ public class RelationTypePresentationFactory : IRelationTypePresentationFactory
private readonly IDocumentPresentationFactory _documentPresentationFactory;
private readonly IScopeProvider _scopeProvider;
[Obsolete("Please use the non obsoleted constructor. Scheduled for removal in v17")]
public RelationTypePresentationFactory(IUmbracoMapper umbracoMapper)
: this(
umbracoMapper,
StaticServiceProvider.Instance.GetRequiredService<IEntityRepository>(),
StaticServiceProvider.Instance.GetRequiredService<IDocumentPresentationFactory>(),
StaticServiceProvider.Instance.GetRequiredService<IScopeProvider>())
{
}
public RelationTypePresentationFactory(
IUmbracoMapper umbracoMapper,
IEntityRepository entityRepository,
@@ -43,6 +43,39 @@ public class UserPresentationFactory : IUserPresentationFactory
/// <summary>
/// Initializes a new instance of the <see cref="UserPresentationFactory"/> class.
/// </summary>
[Obsolete("Please use the constructor taking all parameters. Scheduled for removal in Umbraco 17.")]
public UserPresentationFactory(
IEntityService entityService,
AppCaches appCaches,
MediaFileManager mediaFileManager,
IImageUrlGenerator imageUrlGenerator,
IUserGroupPresentationFactory userGroupPresentationFactory,
IAbsoluteUrlBuilder absoluteUrlBuilder,
IEmailSender emailSender,
IPasswordConfigurationPresentationFactory passwordConfigurationPresentationFactory,
IOptionsSnapshot<SecuritySettings> securitySettings,
IBackOfficeExternalLoginProviders externalLoginProviders)
: this(
entityService,
appCaches,
mediaFileManager,
imageUrlGenerator,
userGroupPresentationFactory,
absoluteUrlBuilder,
emailSender,
passwordConfigurationPresentationFactory,
securitySettings,
externalLoginProviders,
StaticServiceProvider.Instance.GetRequiredService<IUserService>(),
StaticServiceProvider.Instance.GetRequiredService<IContentService>(),
StaticServiceProvider.Instance.GetRequiredService<IEnumerable<IPermissionPresentationMapper>>())
{
}
/// <summary>
/// Initializes a new instance of the <see cref="UserPresentationFactory"/> class.
/// </summary>
[Obsolete("Please use the constructor taking all parameters. Scheduled for removal in Umbraco 17.")]
public UserPresentationFactory(
IEntityService entityService,
AppCaches appCaches,
@@ -54,6 +87,45 @@ public class UserPresentationFactory : IUserPresentationFactory
IPasswordConfigurationPresentationFactory passwordConfigurationPresentationFactory,
IOptionsSnapshot<SecuritySettings> securitySettings,
IBackOfficeExternalLoginProviders externalLoginProviders,
IUserService userService,
IContentService contentService)
: this(
entityService,
appCaches,
mediaFileManager,
imageUrlGenerator,
userGroupPresentationFactory,
absoluteUrlBuilder,
emailSender,
passwordConfigurationPresentationFactory,
securitySettings,
externalLoginProviders,
userService,
contentService,
StaticServiceProvider.Instance.GetRequiredService<IEnumerable<IPermissionPresentationMapper>>())
{
}
// TODO (V17): Remove the unused userService and contentService parameters from this constructor.
/// <summary>
/// Initializes a new instance of the <see cref="UserPresentationFactory"/> class.
/// </summary>
public UserPresentationFactory(
IEntityService entityService,
AppCaches appCaches,
MediaFileManager mediaFileManager,
IImageUrlGenerator imageUrlGenerator,
IUserGroupPresentationFactory userGroupPresentationFactory,
IAbsoluteUrlBuilder absoluteUrlBuilder,
IEmailSender emailSender,
IPasswordConfigurationPresentationFactory passwordConfigurationPresentationFactory,
IOptionsSnapshot<SecuritySettings> securitySettings,
IBackOfficeExternalLoginProviders externalLoginProviders,
#pragma warning disable IDE0060 // Remove unused parameter - need to keep these until the next major to avoid breaking changes and/or ambiguous constructor errors
IUserService userService,
IContentService contentService,
#pragma warning restore IDE0060 // Remove unused parameter
IEnumerable<IPermissionPresentationMapper> permissionPresentationMappers)
{
_entityService = entityService;
@@ -12,31 +12,27 @@ using Umbraco.Extensions;
namespace Umbraco.Cms.Api.Management.Install;
public class PostUnattendedInstallNotificationHandler : INotificationAsyncHandler<UnattendedInstallNotification>
public class CreateUnattendedUserNotificationHandler : INotificationAsyncHandler<UnattendedInstallNotification>
{
private readonly IServiceScopeFactory _serviceScopeFactory;
private readonly IOptions<UnattendedSettings> _unattendedSettings;
private readonly IUserService _userService;
private readonly IMetricsConsentService _metricsConsentService;
public PostUnattendedInstallNotificationHandler(IOptions<UnattendedSettings> unattendedSettings, IUserService userService, IServiceScopeFactory serviceScopeFactory, IMetricsConsentService metricsConsentService)
public CreateUnattendedUserNotificationHandler(IOptions<UnattendedSettings> unattendedSettings, IUserService userService, IServiceScopeFactory serviceScopeFactory)
{
_unattendedSettings = unattendedSettings;
_userService = userService;
_serviceScopeFactory = serviceScopeFactory;
_metricsConsentService = metricsConsentService;
}
/// <summary>
/// Listening for when the UnattendedInstallNotification fired after a successful unattended install
/// This creates the user and sets the telemetry level based on the 'Unattended' settings.
/// Listening for when the UnattendedInstallNotification fired after a sucessfulk
/// </summary>
/// <param name="notification"></param>
/// <param name="cancellationToken"></param>
public async Task HandleAsync(UnattendedInstallNotification notification, CancellationToken cancellationToken)
{
UnattendedSettings? unattendedSettings = _unattendedSettings.Value;
// Ensure we have the setting enabled (Sanity check)
// In theory this should always be true as the event only fired when a sucessfull
if (_unattendedSettings.Value.InstallUnattended == false)
@@ -87,7 +83,7 @@ public class PostUnattendedInstallNotificationHandler : INotificationAsyncHandle
$"No user found in membership provider with id of {Constants.Security.SuperUserIdAsString}.");
}
// To change the password here we actually need to reset it since we don't have an old one to use to change
//To change the password here we actually need to reset it since we don't have an old one to use to change
var resetToken = await backOfficeUserManager.GeneratePasswordResetTokenAsync(membershipUser);
if (string.IsNullOrWhiteSpace(resetToken))
{
@@ -102,7 +98,5 @@ public class PostUnattendedInstallNotificationHandler : INotificationAsyncHandle
throw new InvalidOperationException("Could not reset password: " +
string.Join(", ", resetResult.Errors.ToErrorMessage()));
}
await _metricsConsentService.SetConsentLevelAsync(unattendedSettings.UnattendedTelemetryLevel);
}
}
@@ -22,6 +22,17 @@ public class DocumentPermissionMapper : IPermissionPresentationMapper, IPermissi
private readonly Lazy<IEntityService> _entityService;
private readonly Lazy<IUserService> _userService;
/// <summary>
/// Initializes a new instance of the <see cref="DocumentPermissionMapper"/> class.
/// </summary>
[Obsolete("Please use the constructor taking all parameters. Scheduled for removal in Umbraco 17.")]
public DocumentPermissionMapper()
: this(
StaticServiceProvider.Instance.GetRequiredService<Lazy<IEntityService>>(),
StaticServiceProvider.Instance.GetRequiredService<Lazy<IUserService>>())
{
}
/// <summary>
/// Initializes a new instance of the <see cref="DocumentPermissionMapper"/> class.
/// </summary>
@@ -21,6 +21,18 @@ public class BackOfficeAuthorizationInitializationMiddleware : IMiddleware
private readonly IRuntimeState _runtimeState;
private readonly WebRoutingSettings _webRoutingSettings;
[Obsolete("Use the non-obsolete constructor. This will be removed in Umbraco 17.")]
public BackOfficeAuthorizationInitializationMiddleware(
UmbracoRequestPaths umbracoRequestPaths,
IServiceProvider serviceProvider,
IRuntimeState runtimeState,
IOptions<GlobalSettings> globalSettings,
IOptions<WebRoutingSettings> webRoutingSettings,
IHostingEnvironment hostingEnvironment)
: this(umbracoRequestPaths, serviceProvider, runtimeState, webRoutingSettings)
{
}
public BackOfficeAuthorizationInitializationMiddleware(
UmbracoRequestPaths umbracoRequestPaths,
IServiceProvider serviceProvider,
File diff suppressed because it is too large Load Diff
@@ -1,6 +1,6 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.OpenApi;
using Microsoft.OpenApi.Models;
using Swashbuckle.AspNetCore.SwaggerGen;
using Umbraco.Cms.Api.Management.DependencyInjection;
using Umbraco.Extensions;
@@ -21,17 +21,27 @@ public abstract class BackOfficeSecurityRequirementsOperationFilterBase : IOpera
if (!context.MethodInfo.GetCustomAttributes(true).Any(x => x is AllowAnonymousAttribute) &&
!(context.MethodInfo.DeclaringType?.GetCustomAttributes(true).Any(x => x is AllowAnonymousAttribute) ?? false))
{
operation.Responses ??= new OpenApiResponses();
operation.Responses.Add(
StatusCodes.Status401Unauthorized.ToString(),
new OpenApiResponse
{
Description = "The resource is protected and requires an authentication token",
});
operation.Responses.Add(StatusCodes.Status401Unauthorized.ToString(), new OpenApiResponse
{
Description = "The resource is protected and requires an authentication token"
});
var schemaRef = new OpenApiSecuritySchemeReference(ManagementApiConfiguration.ApiSecurityName, context.Document);
operation.Security ??= new List<OpenApiSecurityRequirement>();
operation.Security.Add(new OpenApiSecurityRequirement { [schemaRef] = [] });
operation.Security = new List<OpenApiSecurityRequirement>
{
new OpenApiSecurityRequirement
{
{
new OpenApiSecurityScheme
{
Reference = new OpenApiReference
{
Type = ReferenceType.SecurityScheme,
Id = ManagementApiConfiguration.ApiSecurityName
}
}, []
}
}
};
}
// Assuming if and endpoint have more then one AuthorizeAttribute, there is a risk the user do not have access while still being authorized.
@@ -47,13 +57,10 @@ public abstract class BackOfficeSecurityRequirementsOperationFilterBase : IOpera
if (numberOfAuthorizeAttributes > 2 || hasConstructorInjectingIAuthorizationService)
{
operation.Responses ??= new OpenApiResponses();
operation.Responses.Add(
StatusCodes.Status403Forbidden.ToString(),
new OpenApiResponse
{
Description = "The authenticated user does not have access to this resource",
});
operation.Responses.Add(StatusCodes.Status403Forbidden.ToString(), new OpenApiResponse()
{
Description = "The authenticated user does not have access to this resource"
});
}
}
}
@@ -1,5 +1,5 @@
using Microsoft.AspNetCore.Http;
using Microsoft.OpenApi;
using Microsoft.OpenApi.Models;
using Swashbuckle.AspNetCore.SwaggerGen;
using Umbraco.Cms.Api.Management.ViewModels;
using Umbraco.Cms.Core;
@@ -24,25 +24,28 @@ internal sealed class NotificationHeaderFilter : IOperationFilter
// filter out irrelevant responses (401 will never produce notifications)
IEnumerable<OpenApiResponse> relevantResponses = operation
.Responses?
.Responses
.Where(pair => pair.Key != StatusCodes.Status401Unauthorized.ToString())
.Select(pair => pair.Value)
.OfType<OpenApiResponse>()
?? Enumerable.Empty<OpenApiResponse>();
.Select(pair => pair.Value);
foreach (OpenApiResponse response in relevantResponses)
{
response.Headers ??= new Dictionary<string, IOpenApiHeader>();
response.Headers.TryAdd(
Constants.Headers.Notifications,
new OpenApiHeader
response.Headers.TryAdd(Constants.Headers.Notifications, new OpenApiHeader
{
Description = "The list of notifications produced during the request.",
Schema = new OpenApiSchema
{
Description = "The list of notifications produced during the request.",
Schema = new OpenApiSchema
Type = "array",
Nullable = true,
Items = new OpenApiSchema()
{
Type = JsonSchemaType.Array | JsonSchemaType.Null,
Items = new OpenApiSchemaReference(notificationModelType.Name),
},
});
Reference = new OpenApiReference()
{
Type = ReferenceType.Schema,
Id = notificationModelType.Name
},
}
}
});
}
}
}

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