Compare commits

..
Author SHA1 Message Date
Andreas ZerbstandGitHub 9ab0abca99 E2E: Stop running version 14 on nightly test pipeline (#20558)
Stops 14 from running on our nightly test pipeline
2025-10-20 12:05:42 +02:00
Andy Butland 009a999e22 Merge branch 'release/14.3.4' into v14/dev 2025-04-08 07:15:06 +02:00
Andy ButlandandGitHub d3c1443b14 Merge commit from fork
* Prevent path traveral vulnerability with upload of temporary files.

* Used BadRequest instead of NotFound for invalid file name response.
2025-04-08 05:03:40 +02:00
Andy Butland 8a9db11422 Merge branch 'release/14.3.3' into v14/dev
# Conflicts:
#	Directory.Packages.props
#	version.json
2025-03-11 09:28:52 +01:00
Andy ButlandandGitHub d9fb6df16e Merge commit from fork
* Bumped version to 15.2.1.

# Conflicts:
#	version.json

* Tighten management API endpoint access rules.
2025-03-11 05:11:35 +01:00
Andy Butland 14ed3348bf bumped imagesharp to prevent CVE-2025-27598 (#18602)
# Conflicts:
#	Directory.Packages.props
2025-03-09 08:54:03 +01:00
Sven Geusens 9e7a36865a Merge branch 'v13/dev' into v14/dev
Revert #18249 as it is reimplemented for v15
Revert #18320 as the new architecture explictly throws an error

# Conflicts:
#	build/azure-pipelines.yml
#	src/Umbraco.Core/EmbeddedResources/Lang/en.xml
#	src/Umbraco.Core/EmbeddedResources/Lang/en_us.xml
#	src/Umbraco.Core/Models/ContentEditing/ContentSaveAction.cs
#	src/Umbraco.Core/Services/ContentService.cs
#	src/Umbraco.Core/Services/IContentService.cs
#	src/Umbraco.Core/Services/MemberService.cs
#	src/Umbraco.Infrastructure/PropertyEditors/RichTextEditorPastedImages.cs
#	src/Umbraco.Infrastructure/Security/BackOfficeUserStore.cs
#	src/Umbraco.Infrastructure/Security/MemberUserStore.cs
#	src/Umbraco.Web.BackOffice/Controllers/ContentController.cs
#	src/Umbraco.Web.BackOffice/Controllers/EntityController.cs
#	src/Umbraco.Web.BackOffice/Controllers/MediaController.cs
#	src/Umbraco.Web.BackOffice/Controllers/MemberController.cs
#	src/Umbraco.Web.BackOffice/Controllers/PreviewController.cs
#	src/Umbraco.Web.BackOffice/Controllers/UsersController.cs
#	src/Umbraco.Web.BackOffice/DependencyInjection/UmbracoBuilder.BackOfficeAuth.cs
#	src/Umbraco.Web.BackOffice/Filters/ContentSaveValidationAttribute.cs
#	src/Umbraco.Web.BackOffice/Filters/MemberSaveModelValidator.cs
#	src/Umbraco.Web.BackOffice/Filters/MemberSaveValidationAttribute.cs
#	src/Umbraco.Web.BackOffice/Trees/ContentTreeController.cs
#	src/Umbraco.Web.Common/RuntimeMinification/SmidgeOptionsSetup.cs
#	src/Umbraco.Web.Common/RuntimeMinification/SmidgeRuntimeMinifier.cs
#	src/Umbraco.Web.Common/Views/UmbracoViewPage.cs
#	src/Umbraco.Web.UI.Client/src/common/directives/components/buttons/umbbuttongroup.directive.js
#	src/Umbraco.Web.UI.Client/src/common/directives/components/content/edit.controller.js
#	src/Umbraco.Web.UI.Client/src/common/directives/components/content/umbtabbedcontent.directive.js
#	src/Umbraco.Web.UI.Client/src/common/directives/components/property/umbproperty.directive.js
#	src/Umbraco.Web.UI.Client/src/common/resources/content.resource.js
#	src/Umbraco.Web.UI.Client/src/common/resources/publicaccess.resource.js
#	src/Umbraco.Web.UI.Client/src/common/resources/users.resource.js
#	src/Umbraco.Web.UI.Client/src/common/services/assets.service.js
#	src/Umbraco.Web.UI.Client/src/views/common/drawers/help/help.controller.js
#	src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/linkpicker/linkpicker.controller.js
#	src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/mediaentryeditor/mediaentryeditor.controller.js
#	src/Umbraco.Web.UI.Client/src/views/components/content/umb-tabbed-content.html
#	src/Umbraco.Web.UI.Client/src/views/components/property/umb-property.html
#	src/Umbraco.Web.UI.Client/src/views/content/content.create.controller.js
#	src/Umbraco.Web.UI.Client/src/views/content/overlays/publishdescendants.controller.js
#	src/Umbraco.Web.UI.Client/src/views/content/overlays/publishdescendants.html
#	src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/listview.controller.js
#	src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/rte.component.js
#	src/Umbraco.Web.UI.Client/src/views/users/views/user/details.html
#	src/Umbraco.Web.UI.Client/src/views/webhooks/edit.controller.js
#	src/Umbraco.Web.UI.Client/src/views/webhooks/edit.html
#	src/Umbraco.Web.UI.Client/test/unit/app/content/create-content-controller.spec.js
#	src/Umbraco.Web.UI.Client~HEAD
#	src/Umbraco.Web.UI.Login/src/auth.element.ts
#	tests/Umbraco.TestData/UmbracoTestDataController.cs
#	tests/Umbraco.Tests.Integration/Umbraco.Core/Services/ContentServiceTests.cs
#	tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentEventsTests.cs
#	tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentServicePublishBranchTests.cs
#	tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentServiceTagsTests.cs
#	tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Security/MemberUserStoreTests.cs
#	tests/Umbraco.Tests.UnitTests/Umbraco.Web.BackOffice/Controllers/MemberControllerUnitTests.cs
#	version.json
2025-02-17 19:25:45 +01:00
Andy ButlandandGitHub db1d999721 Avoid exception when attempting to find member by Id when Id is not an expected Guid or integer, as can be the case with external member providers. (#18320) 2025-02-17 12:47:52 +01:00
Andy ButlandandGitHub 18047a7cfb Only filter post retrieval of entities for start nodes if working with entities that support start nodes. (#18287) 2025-02-17 12:45:51 +01:00
Andy ButlandandGitHub def7ebd48c Html encodes the user's name in the invite email. (#18343) 2025-02-17 12:25:12 +01:00
8c2b1ebdc5 V13: Introduce publishNotifications method on IMembershipMemberService (#18207)
* Introduce publishNotifications method on IMembershipMemberService.cs

* Fix test

* Add PublishNotificationSaveOptions

* Fix up according to comments

* Use numeric values for flag enum

* Update src/Umbraco.Core/Services/MemberService.cs

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

* Update src/Umbraco.Core/Services/MemberService.cs

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

---------

Co-authored-by: Andy Butland <abutland73@gmail.com>
2025-02-12 13:30:41 +01:00
Andy ButlandandGitHub 9227517a50 Split force for publish descendants into separate options for publish unpublish and re-publish unedited (13) (#18249)
* Split force for publish descendents into separate options for publish unpublish and re-publish unedited.

* Added integration task verifying updated behaviour.

* Variant integration test.

* Update test data controller.

* Remove usued function parameters.

* Refactor to enum.

* Fixed flags enum.

* Variable name refactor.

* Applied changes from code review.

* Refactored method name.

* Aligned js boolean checks.
2025-02-12 11:30:27 +00:00
Andy Butland 5322d0f7b5 Bumped version to 13.8.0-rc. 2025-02-12 08:13:58 +01:00
Andy ButlandandGitHub a282cc5691 Backport use of thread delay over sleep and handle dispose in FileSystemMainDomLock (#18151)
* Backport use of thread delay over sleep and handle dispose in FileSystemMainDomLock (from PRs #18119 and #18147)

* Applied suggestion from code review.
2025-02-12 08:06:50 +01:00
Andy ButlandandGitHub 048f8bcdf9 Fixed userResource request to get all users. (#18105) 2025-02-11 17:06:18 +01:00
Andy ButlandandGitHub 7bcbc748d4 URL encodes member user names when passing information for public access setting such that those with user names as emails containing a plus will be included in the rule. (#18142) 2025-02-11 13:36:19 +01:00
Andy ButlandandGitHub f30e6cfe7f Prevents folder selection in media picker when used from the multi URL picker. (#18288) 2025-02-11 09:18:01 +01:00
Andy ButlandandGitHub 026e80e026 Avoid an exception on sign out when the principal is populated from an incomplete external login (#18078)
* Avoid an exception on signout when the principal is populated from an incomplete external login.

* Tidied up comment.
2025-02-10 12:51:35 +01:00
Andy ButlandandGitHub a3b77cff63 Add validation to prevent update of a user or member to an invalid username (13) (#18261)
* Add validation to prevent update of a user or member to an invalid username.

* Avoid password manager updates of user name field on user details screen.
2025-02-10 10:40:53 +01:00
Niels LyngsøandGitHub 4ca68d6995 Add client-side validation for RTE (#18257) 2025-02-07 11:43:04 +00:00
Sven GeusensandGitHub 280cb7f2b1 Fix issues in newly added buttongroup localization (#18254)
* Fix #18253 nullref exception

* Fix #18239 by listening to broader scope changes
2025-02-07 07:00:34 +01:00
095a73132c Review: Allow Duplicate Email for Members (#16202)
* init

* Aligned default values on security settings.

* Added validator for security settings.

* Provide default implementation for get members by email.

* Refactored constructor of MemberController.

* Validate on unique member email only when configured to do so.

* Further code tidy and use of DI in constructor.

* Used new constructor in tests.

* Add unit test for modified behaviour.

* Removed validator for security settings (it's not necessary, I got confused with users and members).

* Spelling.

---------

Co-authored-by: Andy Butland <abutland73@gmail.com>
2025-02-05 12:38:40 +01:00
Andy Butland cfb0fc23ac Merge branch 'v13/dev' into v13/contrib 2025-02-04 12:35:49 +01:00
6620aca9fe Cache null dictionary values by key (#15576)
* Add CacheNullValues option to RepositoryCachePolicy

* Cache null values in DictionaryByKeyRepository

* Fixed issue with nullable reference.

* Updated logic for caching of null values.

* Update src/Umbraco.Infrastructure/Cache/DefaultRepositoryCachePolicy.cs

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

* Made the NullValueRepresentation overwritable in a generic manner

* Improve generic NullValueCachePolicyResolver

* Revert Commits and clarify logic with comment

This reverts commit 8befb43792 "Improve generic NullValueCachePolicyResolver"
Also reverts 8adf0a2 - Made the NullValueRepresentation overwritable in a generic manner
And 8adf0a2 - Made the NullValueRepresentation overwritable in a generic manner

* Update src/Umbraco.Infrastructure/Cache/DefaultRepositoryCachePolicy.cs

---------

Co-authored-by: Andy Butland <abutland73@gmail.com>
Co-authored-by: Sven Geusens <geusens@gmail.com>
Co-authored-by: Sven Geusens <sge@umbraco.dk>
2025-02-04 12:29:21 +01:00
2422a02b23 Set Smidge cachebuster type (#18198)
* Set Smidge cachebuster type

* Amend exception and fix typo

* Minor tweak to comment and exception message.

---------

Co-authored-by: Simon Hartfield <simon.hartfield@googlemail.com>
Co-authored-by: Andy Butland <abutland73@gmail.com>
2025-02-04 11:00:44 +00:00
b4a9dc0770 V13: Fix members while using basic auth. (#18206)
* Flow additional identities to new principal

* Add extension to more easily get member identity

* Ensure the member is used instead of the backoffice user in `MemberManager`

* Update snippet

* Fix the comment that I broke

* Update src/Umbraco.Web.Common/Extensions/MemberClaimsPrincipalExtensions.cs

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

---------

Co-authored-by: Andy Butland <abutland73@gmail.com>
2025-02-03 19:48:08 +01:00
Andy ButlandandGitHub e7411244fd Show notifications menu only to users with permission for the feature. (#18184) 2025-02-03 13:24:58 +01:00
Kenn JacobsenandGitHub 9f357173c3 Enforce user start nodes for media uploads through the RTE (#18204) 2025-02-03 12:50:23 +01:00
Andy Butland 7dc6e3ed5b Merge branch 'v13/dev' into v13/contrib 2025-01-31 16:16:22 +01:00
Gareth WrightandGitHub 5dfff212d0 Update auth.element.ts (#18192)
auth_username => general_username
2025-01-31 14:59:25 +01:00
17615f966b Fix out of memory of 2gb+ (max 4gb) error introduced by #14657 SVG xss security fix (#17421)
Co-authored-by: Andy Butland <abutland73@gmail.com>
2025-01-31 13:46:56 +00:00
Kenn JacobsenandGitHub 7a2d6b6c63 More robust resolving of Delivery API redirects (#18160) 2025-01-31 14:26:07 +01:00
Sven Geusens 343867f6ee Merge branch 'release/13.6' into v13/dev and version bump
# Conflicts:
#	src/Umbraco.Web.BackOffice/Controllers/PreviewController.cs
#	version.json
2025-01-30 16:27:49 +01:00
Bjarke BergandSven Geusens b9837ac77c Optimize Azure pipeline (#17674)
* Only fetch single commit

* Hopefully fixes Nerdbank.GitVersioning.GitException: Shallow clone lacks the objects required to calculate version height. Use full clones or clones with a history at least as deep as the last version height resetting change.

* Do not checkout again

* More test pipeline

* Another attempt

* yet another attempt

* more attempts

* Revert "more attempts"

This reverts commit 5694d97ba6.

* Test without building backoffice and login explicitly

* Fix mem leak in integration tests

* Fixes sqlserver lock test
# Conflicts:
#	build/azure-pipelines.yml
2025-01-30 14:10:05 +01:00
Sven Geusens bb73ec6c68 Set release version 2025-01-30 09:52:50 +01:00
Andy ButlandandGitHub ee2d7bbb1b Excluded tags from trashed content (#18164) 2025-01-29 21:10:11 +01:00
Andy ButlandandGitHub 5d48bc7371 Provides an option to remove the inessential version number from the generated models (#18081)
* Provides an option to remove the inessential version number from the generated models.

* Clarified comment.
2025-01-29 13:33:49 +01:00
Kenn JacobsenandGitHub 59a4649528 Do not allow editing read-only properties by clicking their labels (#18152)
* Do not allow editing read-only properties by clicking their labels

* Simplify the fix :)

* Fix linting issue
2025-01-29 10:29:58 +01:00
Andy ButlandandGitHub f54b6033ab Added Resharper test assemblies to exclude list on TypeFinder. (#18145) 2025-01-29 10:27:17 +01:00
Jacob OvergaardandGitHub bf340cd7d4 fix: remove unused parameters and documentation (#18095)
this fixes an issue where unused parameters were published as supported, but they were in fact never supported.
2025-01-28 10:53:00 +00:00
Andy Butland 313417cb91 Tidied up XML header comment in ITagQuery. 2025-01-28 10:52:21 +01:00
7850078623 Redirect to the published URL when exiting preview (#18114)
Co-authored-by: Andy Butland <abutland73@gmail.com>
2025-01-24 18:46:28 +00:00
44bf3b77b3 Make it possible to reset media picker crops (#18110)
Co-authored-by: Andy Butland <abutland73@gmail.com>
2025-01-24 13:41:30 +00:00
Kenn JacobsenandGitHub 8485458896 Add clientside validation to webhook events (#18089) 2025-01-23 17:01:55 +01:00
Nikolaj Geisle 3d253f5f06 bump version 2025-01-23 11:15:26 +01:00
Nikolaj Geisle 64287d7e1a Merge branch 'release/13.5.3' into release/13.6
# Conflicts:
#	version.json
2025-01-23 11:14:47 +01:00
Andy ButlandandGitHub edc78a5a4c Handles migration case where an expected constraint is renamed but the constraint does not exist. (#18063) 2025-01-22 12:26:06 +01:00
Andy Butland 8056a59a10 Fix schema defined in JSON schema tool definition after merge from 13. 2025-01-21 15:52:31 +01:00
1a18d6c035 Replaced deprecated navigator.platform with navigator.userAgent for platform detection. (#17373)
Co-authored-by: Niels Lyngsø <nsl@umbraco.dk>
2025-01-21 13:56:40 +01:00
Andy Butland 3e877182b5 Merge branch 'v13/contrib' into v13/dev 2025-01-21 09:47:20 +01:00
Martin VennevoldandGitHub 0e4f883bc1 Fix create child issue in list view with infinite editor (#13355). (#17637) 2025-01-21 09:44:22 +01:00
Andy Butland 265cc62ab8 Merge branch 'release/13.6' into v13/dev 2025-01-21 09:21:36 +01:00
Andy Butland 800c16970e Merge branch 'release/13.6' of https://github.com/umbraco/Umbraco-CMS into release/13.6 2025-01-21 09:20:11 +01:00
Andy Butland 6bd11bf233 Fixes failing front-end unit test to align with new behaviour from PR #17818 (
Allow skipSelect blueprints only when one blueprint exists).
2025-01-21 09:19:31 +01:00
Kenn Jacobsen a2fd82a3f3 Add NoopCurrentMemberClaimsProvider so Umbraco can boot without the Delivery API enabled (#18049) 2025-01-21 09:13:31 +01:00
Kenn JacobsenandGitHub 7567990da1 Add NoopCurrentMemberClaimsProvider so Umbraco can boot without the Delivery API enabled (#18049) 2025-01-21 09:10:43 +01:00
Zeegaan c7d157bbfd build on ubuntu 2025-01-21 09:02:00 +01:00
Jacob Overgaard abc312c9b4 update backoffice submodule to 14.3.2 2025-01-21 08:44:10 +01:00
Nikolaj Geisle 2161edb871 Bump version 2025-01-21 07:41:06 +01:00
Nikolaj Geisle e77e9c5691 Format sql statement (#17354)
(cherry picked from commit aa9f194d76)
2025-01-20 18:27:54 +01:00
Nikolaj Geisle e934a943b5 build on windows 2025-01-20 17:37:04 +01:00
Ronald BarendseandNikolaj Geisle 65bb2801b0 Merge commit from fork
* Add TimedScope

* Use TimedScope in login endpoint

* Use seperate default duration and only calculate average of actual successful responses

* Only return detailed error responses if credentials are valid

* Cancel timed scope when credentials are valid

* Add UserDefaultFailedLoginDuration and UserMinimumFailedLoginDuration settings
2025-01-20 16:44:28 +01:00
Andy ButlandandGitHub a0a4af6a0c Merge commit from fork 2025-01-20 14:54:14 +01:00
Jacob Overgaard e8d6cded2b update backoffice submodule to 14.3.2 2025-01-20 14:42:17 +01:00
Bjarke BergandJacob Overgaard c65204a146 update backoffice submodule to 14.3.2 2025-01-20 14:34:03 +01:00
Andy ButlandandGitHub 95eb58587b Merge commit from fork 2025-01-20 14:14:28 +01:00
Andy ButlandandGitHub 86f3033334 Merge commit from fork 2025-01-20 14:14:28 +01:00
25628a8b76 Lucene Package Update to Address CVE-2024-43383 (#17942)
* Update Lucene Package to 4.8.0-beta00017

* Add Package Reference

---------

Co-authored-by: Sebastiaan Janssen <sebastiaan@umbraco.com>
2025-01-20 11:45:35 +01:00
Callum WhyteandAndy Butland 7552e315fb Allow skipSelect blueprints only when one blueprint exists (#17818) 2025-01-20 11:45:26 +01:00
Callum WhyteandGitHub d15b5b221e Allow skipSelect blueprints only when one blueprint exists (#17818) 2025-01-20 10:27:54 +01:00
Andy Butland 0166727eee Bumped version to 14.3.2. 2025-01-07 10:13:02 +01:00
7882 changed files with 26552 additions and 388454 deletions
-1
View File
@@ -46,7 +46,6 @@
*.xml text=auto
*.resx text=auto
*.yml text eol=lf core.whitespace whitespace=tab-in-indent,trailing-space,tabwidth=2
*.sh eol=lf
*.csproj text=auto merge=union
*.vbproj text=auto merge=union
+62 -32
View File
@@ -4,8 +4,8 @@
In order to use Umbraco as a CMS and build your website with it, you should not build it yourself. If you're reading this then you're trying to contribute to Umbraco or you're debugging a complex issue.
- 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?
- 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 download page](https://our.umbraco.com/download) and start using Umbraco CMS as intended.
@@ -13,6 +13,17 @@ If the answer is yes, please read on. Otherwise, make sure to head on over [to t
↖️ You can jump to any section by using the "table of contents" button ( ![Table of contents icon](img/tableofcontentsicon.svg) ) above.
## Getting Started:
To run umbraco, we first need to initialize the client git submodule:
* Execute `git submodule update --init` to get the files into Umbraco.Web.UI.Client project
* If you are going to work on the Backoffice, you can either go to the Umbraco.Web.UI.Client folder and check out a new branch or set it up in your IDE, which will allow you to commit to each repository simultaneously:
* **Rider**: Preferences -> Version Control -> Directory Mappings -> Click the '+' sign
* If you get a white page delete Umbraco.Cms.StaticAssets\wwwroot\umbraco folder and run `npm ci && npm run build:for:cms` inside Umbraco.Web.UI.Client folder to clear out any leftover files from older versions.
### Latest version
* If you want to get the latest changes from the client repository, run `git submodule update` again which will pull the latest main branch.
## Debugging source locally
Did you read ["Are you sure"](#are-you-sure)?
@@ -21,32 +32,29 @@ Did you read ["Are you sure"](#are-you-sure)?
If you want to run a build without debugging, see [Building from source](#building-from-source) below. This runs the build in the same way it is run on our build servers.
> [!NOTE]
> The caching for the back office has been described as 'aggressive' so we often find it's best when making back office changes to [disable caching in the browser (check "Disable cache" on the "Network" tab of developer tools)][disable browser caching] to help you to see the changes you're making.
#### Debugging with VS Code
In order to build the Umbraco source code locally with Visual Studio Code, first make sure you have the following installed.
- [Visual Studio Code](https://code.visualstudio.com/)
- [dotnet SDK v9+](https://dotnet.microsoft.com/en-us/download)
- [Node.js v20+](https://nodejs.org/en/download/)
- npm v10+ (installed with Node.js)
- [Git command line](https://git-scm.com/download/)
* [Visual Studio Code](https://code.visualstudio.com/)
* [dotnet SDK v7+](https://dotnet.microsoft.com/en-us/download)
* [Node.js v14+](https://nodejs.org/en/download/)
* npm v7+ (installed with Node.js)
* [Git command line](https://git-scm.com/download/)
Open the root folder of the repository in Visual Studio Code.
To build the front-end you'll need to open the command pallet (<kbd>Ctrl</kbd> + <kbd>Shift</kbd> + <kbd>P</kbd>) and run `>Tasks: Run Task` followed by `Client Build`.
To build the front end you'll need to open the command pallet (<kbd>Ctrl</kbd> + <kbd>Shift</kbd> + <kbd>P</kbd>) and run `>Tasks: Run Task` followed by `Client Watch` and then run the `Client Build` task in the same way.
You can also run the tasks manually on the command line:
```
cd src\Umbraco.Web.UI.Client
npm i
npm run build:for:cms
npm run dev
```
If you want to make changes to the UI, you can choose to run a front-end development server. To learn more please read the Umbraco.Web.UI.Client README.md [Run against a local Umbraco instance](../src/Umbraco.Web.UI.Client/.github/README.md#run-against-a-local-umbraco-instance) for more information.
If you just want to build the UI Client to `Umbraco.Web.UI` then instead of running `dev`, you can do: `npm run build`.
The login screen is a different frontend build, for that one you can run it as follows:
@@ -58,6 +66,12 @@ npm run dev
If you just want to build the Login screen to `Umbraco.Web.UI` then instead of running `dev`, you can do: `npm run build`.
**The initial Gulp build might take a long time - don't worry, this will be faster on subsequent runs.**
You might run into [Gulp quirks](#gulp-quirks).
The caching for the back office has been described as 'aggressive' so we often find it's best when making back office changes to [disable caching in the browser (check "Disable cache" on the "Network" tab of developer tools)][disable browser caching] to help you to see the changes you're making.
To run the C# portion of the project, either hit <kbd>F5</kbd> to begin debugging, or manually using the command line:
```
@@ -72,18 +86,24 @@ When the page eventually loads in your web browser, you can follow the installer
In order to build the Umbraco source code locally with Visual Studio, first make sure you have the following installed.
- [Visual Studio 2022 v17+ with .NET 7+](https://visualstudio.microsoft.com/vs/) ([the community edition is free](https://www.visualstudio.com/thank-you-downloading-visual-studio/?sku=Community&rel=15) for you to use to contribute to Open Source projects)
- [Node.js v20+](https://nodejs.org/en/download/)
- npm v10+ (installed with Node.js)
- [Git command line](https://git-scm.com/download/)
* [Visual Studio 2022 v17+ with .NET 7+](https://visualstudio.microsoft.com/vs/) ([the community edition is free](https://www.visualstudio.com/thank-you-downloading-visual-studio/?sku=Community&rel=15) for you to use to contribute to Open Source projects)
* [Node.js v14+](https://nodejs.org/en/download/)
* npm v7+ (installed with Node.js)
* [Git command line](https://git-scm.com/download/)
The easiest way to get started is to open `umbraco.sln` in Visual Studio.
Umbraco is a C# based codebase, which is mostly ASP.NET Core MVC based. You can make changes, build them in Visual Studio, and hit <kbd>F5</kbd> to see the result. There are two web projects in the solution, `Umbraco.Web.UI.Client` and `Umbraco.Web.UI.Login`. They each have their own build process and can be run separately. The `Umbraco.Web.UI` project is the main project that hosts the back office and login screen. This is the project you will want to run to see your changes. It will automatically restore and build the client projects when you run it.
To build the front end, you'll first need to run `cd src\Umbraco.Web.UI.Client && npm install` in the command line (or `cd src\Umbraco.Web.UI.Client; npm install` in PowerShell). Then find the Task Runner Explorer (View → Other Windows → Task Runner Explorer) and run the `build` task under `Gulpfile.js`. You may need to refresh the Task Runner Explorer before the tasks load.
If you want to watch the UI Client to `Umbraco.Web.UI` then you can open a terminal in `src/Umbraco.Web.UI.Client` where you can run `npm run dev:mock` manually. This will launch the Vite dev server on http://localhost:5173 and watch for changes with mocked API responses.
If you're working on the backoffice, you may wish to run the `dev` command instead while you're working with it, so changes are copied over to the appropriate directories and you can refresh your browser to view the results of your changes.
You can also run `npm run dev:server` to run the Vite server against a local Umbraco instance. In this case, you need to have the .NET project running and accept connections from the Vite server. Please see the Umbraco.Web.UI.Client README.md [Run against a local Umbraco instance](../src/Umbraco.Web.UI.Client/.github/README.md#run-against-a-local-umbraco-instance) for more information.
**The initial Gulp build might take a long time - don't worry, this will be faster on subsequent runs.**
You might run into [Gulp quirks](#gulp-quirks).
The caching for the back office has been described as 'aggressive' so we often find it's best when making back office changes to [disable caching in the browser (check "Disable cache" on the "Network" tab of developer tools)][disable browser caching] to help you to see the changes you're making.
"The rest" is a C# based codebase, which is mostly ASP.NET Core MVC based. You can make changes, build them in Visual Studio, and hit <kbd>F5</kbd> to see the result.
**The initial C# build might take a _really_ long time (seriously, go and make a cup of coffee!) - but don't worry, this will be faster on subsequent runs.**
@@ -95,7 +115,7 @@ Did you read ["Are you sure"](#are-you-sure)?
Do note that this is only required if you want to test out your custom changes in a separate site (not the one in the Umbraco.Web.UI), if you just want to test your changes you can run the included test site using: `dotnet run` from `src/Umbraco.Web.UI/`
You may want to build a set of NuGet packages with your changes, this can be done using the dotnet pack command.
You may want to build a set of NuGet packages with your changes, this can be done using the dotnet pack command.
First enter the root of the project in a command line environment, and then use the following command to build the NuGet packages:
@@ -107,18 +127,17 @@ You can then add these as a local NuGet feed using the following command:
`dotnet nuget add source <Path to Build.Out folder> -n MyLocalFeed`
This will add a local nuget feed with the name "MyLocalFeed" and you'll now be able to use your custom built NuGet packages.
This will add a local nuget feed with the name "MyLocalFeed" and you'll now be able to use your custom built NuGet packages.
### Cleaning up
Once the solution has been used to run a site, one may want to "reset" the solution in order to run a fresh new site again.
The easiest way to do this by deleting the following files and folders:
* src/Umbraco.Web.UI/appsettings.json
* src/Umbraco.Web.UI/umbraco/Data
- src/Umbraco.Web.UI/appsettings.json
- src/Umbraco.Web.UI/umbraco/Data
You only have to remove the connection strings from the appsettings, but removing the data folder ensures that the sqlite database gets deleted too.
You only have to remove the connection strings from the appsettings, but removing the data folder ensures that the sqlite database gets deleted too.
Next time you run a build the `appsettings.json` file will be re-created in its default state.
@@ -126,14 +145,13 @@ This will leave media files and views around, but in most cases, it will be enou
To perform a more complete clear, you will want to also delete the content of the media, views, scripts... directories.
The following command will force remove all untracked files and directories, whether they are ignored by Git or not. Combined with `git reset` it can recreate a pristine working directory.
The following command will force remove all untracked files and directories, whether they are ignored by Git or not. Combined with `git reset` it can recreate a pristine working directory.
git clean -xdf .
For git documentation see:
- git [clean](https://git-scm.com/docs/git-clean)
- git [reset](https://git-scm.com/docs/git-reset)
* git [clean](<https://git-scm.com/docs/git-clean>)
* git [reset](<https://git-scm.com/docs/git-reset>)
## Azure DevOps
@@ -147,6 +165,18 @@ The produced artifacts are published in a container that can be downloaded from
Git might have issues dealing with long file paths during build. You may want/need to enable `core.longpaths` support (see [this page](https://github.com/msysgit/msysgit/wiki/Git-cannot-create-a-file-or-directory-with-a-long-path) for details).
[ contribution guidelines]: CONTRIBUTING.md "Read the guide to contributing for more details on contributing to Umbraco"
[ starter kits ]: https://our.umbraco.com/packages/?category=Starter%20Kits&version=9 "Browse starter kits available for v9 on Our "
### Gulp Quirks
You may need to run the following commands to set up gulp properly:
```
npm cache clean --force
npm ci
npm run build
```
[ contribution guidelines]: CONTRIBUTING.md "Read the guide to contributing for more details on contributing to Umbraco"
[ starter kits ]: https://our.umbraco.com/packages/?category=Starter%20Kits&version=9 "Browse starter kits available for v9 on Our "
[ disable browser caching ]: https://techwiser.com/disable-cache-google-chrome-firefox "Instructions on how to disable browser caching in Chrome and Firefox"
-30
View File
@@ -1,30 +0,0 @@
# Bellissima release instructions
## Build
> _See internal documentation on the build/release workflow._
## GitHub Release Notes
To generate release notes on GitHub.
- Go to the [**Releases** area](https://github.com/umbraco/Umbraco.CMS.Backoffice/releases)
- Press the [**"Draft a new release"** button](https://github.com/umbraco/Umbraco.CMS.Backoffice/releases/new)
- In the combobox for "Choose a tag", expand then select or enter the next version number, e.g. `release-14.2.0`
- If the tag does not already exist, an option labelled "Create new tag: release-14.2.0 on publish" will appear, select that option
- In the combobox for "Target: main", expand then select the release branch for the next version, e.g. `release/14.2`
- In the combobox for "Previous tag: auto":
- If the next release is an RC, then you can leave as `auto`
- Otherwise, select the previous stable version, e.g. `release-14.1.1`
- Press the **"Generate release notes"** button, this will populate the main textarea
- Change the title to match the version, e.g. `14.2.0`
- Check the details, view in the "Preview" tab
- What type of release is this?
- If it's an RC, then check "Set as a pre-release"
- If it's stable, then check "Set as the latest release"
- Once you're happy with the contents and ready to save...
- If you need more time to review, press the **"Save draft"** button and you can come back to it later
- If you are ready to make the release notes public, then press **"Publish release"** button! :tada:
> If you're curious about how the content is generated, take a look at the `release.yml` configuration:
> https://github.com/umbraco/Umbraco.CMS.Backoffice/blob/main/.github/release.yml
-230
View File
@@ -1,230 +0,0 @@
# Contribution Guidelines
## Thoughts, links, and questions
In the high probability that you are porting something from angular JS then here are a few helpful tips for using Lit:
Here is the LIT documentation and playground: [https://lit.dev](https://lit.dev)
### What is the process of contribution?
- Read the [README](README.md) to learn how to get the project up and running
- Find an issue marked as [community/up-for-grabs](https://github.com/umbraco/Umbraco.CMS.Backoffice/issues?q=is%3Aissue+is%3Aopen+label%3Acommunity%2Fup-for-grabs) - note that some are also marked [good first issue](https://github.com/umbraco/Umbraco.CMS.Backoffice/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22) which indicates they are simple to get started on
- Umbraco HQ owns the Management API on the backend, so features can be worked on in the frontend only when there is an API, or otherwise if no API is required
- A contribution should be made in a fork of the repository
- Once a contribution is ready, a pull request should be made to this repository and HQ will assign a reviewer
- A pull request should always indicate what part of a feature it tries to solve, i.e. does it close the targeted issue (if any) or does the developer expect Umbraco HQ to take over
## Contributing in general terms
A lot of the UI has already been migrated to the new backoffice. Generally speaking, one would find a feature on the projects board, locate the UI in the old backoffice (v11 is fine), convert it to Lit components using the UI library, put the business logic into a store/service, write tests, and make a pull request.
We are also very keen to receive contributions towards **documentation, unit testing, package development, accessibility, and just general testing of the UI.**
## The Management API
The management API is the colloquial term used to describe the new backoffice API. It is built as a .NET Web API, has a Swagger endpoint (/umbraco/swagger), and outputs an OpenAPI v3 schema, that the frontend consumes.
The frontend has an API formatter that takes the OpenAPI schema file and converts it into a set of TypeScript classes and interfaces.
**Current schema for API:**
[https://raw.githubusercontent.com/umbraco/Umbraco-CMS/v13/dev/src/Umbraco.Cms.Api.Management/OpenApi.json](https://raw.githubusercontent.com/umbraco/Umbraco-CMS/v15/dev/src/Umbraco.Cms.Api.Management/OpenApi.json)
**How to convert it:**
- Run `npm run generate:server-api`
## A contribution example
### Example: Published Cache Status Dashboard
![Published Status Dashboard](/.github/img/contributing/published-cache-status-dashboard.png)
### Boilerplate (example using Lit)
Links for Lit examples and documentation:
- [https://lit.dev](https://lit.dev)
- [https://lit.dev/docs/](https://lit.dev/docs/)
- [https://lit.dev/playground/](https://lit.dev/playground/)
### Functionality
**HTML**
The simplest approach is to copy over the HTML from the old backoffice into a new Lit element (check existing elements in the repository, e.g. if you are working with a dashboard, then check other dashboards, etc.). Once the HTML is inside the `render` method, it is often enough to simply replace `<umb-***>` elements with `<uui-***>` and replace a few of the attributes. In general, we try to build as much UI with Umbraco UI Library as possible.
**Controller**
The old AngularJS controllers will have to be converted into modern TypeScript and will have to use our new services and stores. We try to abstract as much away as possible, and mostly you will have to make API calls and let the rest of the system handle things like error handling and so on. In the case of this dashboard, we only have a few GET and POST requests. Looking at the new Management API, we find the PublishedCacheService, which is the new API controller to serve data to the dashboard.
To make the first button work, which simply just requests a new status from the server, we must make a call to `PublishedCacheService.getPublishedCacheStatus()`. An additional thing here is to wrap that in a friendly function called `tryExecuteAndNotify`, which is something we make available to developers to automatically handle the responses coming from the server and additionally use the Notifications to notify of any errors:
```typescript
import { tryExecuteAndNotify } from '@umbraco-cms/backoffice/resources';
import { PublishedCacheService } from '@umbraco-cms/backoffice/external/backend-api';
private _getStatus() {
const { data: status } = await tryExecuteAndNotify(this, PublishedCacheService.getPublishedCacheStatus());
if (status) {
// we now have the status
console.log(status);
}
}
```
### State (buttons, etc)
It is a good idea to make buttons indicate a loading state when awaiting an API call. All `<uui-button>` support the `.state` property, which you can set around API calls:
```typescript
@state()
private _buttonState: UUIButtonState = undefined;
private _getStatus() {
this._buttonState = 'waiting';
[...await...]
this._buttonState = 'success';
}
```
## Making the dashboard visible
### Add to internal manifests
All items are declared in a `manifests.ts` file, which is located in each section directory.
To declare the Published Cache Status Dashboard as a new manifest, we need to add the section as a new json object that would look like this:
```typescript
{
type: 'dashboard',
alias: 'Umb.Dashboard.PublishedStatus',
name: 'Published Status Dashboard',
elementName: 'umb-dashboard-published-status',
element: () => import('./published-status/dashboard-published-status.element.js'),
weight: 200,
meta: {
label: 'Published Status',
pathname: 'published-status',
},
conditions: [
{
alias: 'Umb.Condition.SectionAlias',
match: 'Umb.Section.Settings',
},
],
},
```
Lets go through each of these properties…
- Type: can be one of the following:
- section - examples include: `Content`, `Media`
- dashboard - a view within a section. Examples include: the welcome dashboard
- propertyEditorUi
- editorView
- propertyAction
- tree
- editor
- treeItemAction
- Alias: is the unique key used to identify this item.
- Name: is the human-readable name for this item.
- ElementName: this is the customElementName declared on the element at the top of the file i.e
```typescript
@customElement('umb-dashboard-published-status')
```
- Js: references a function call to import the file that the element is declared within
- Weight: allows us to specify the order in which the dashboard will be displayed within the tabs bar
- Meta: allows us to reference additional data - in our case, we can specify the label that is shown in the tabs bar and the pathname that will be displayed in the URL
- Conditions: allows us to specify the conditions that must be met for the dashboard to be displayed. In our case, we are specifying that the dashboard will only be displayed within the Settings section
## API mock handlers
Running the app with `npm run dev`, you will quickly notice the API requests turn into 404 errors. To hit the API, we need to add a mock handler to define the endpoints that our dashboard will call. In the case of the Published Cache Status section, we have several calls to work through. Lets start by looking at the call to retrieve the current status of the cache:
![Published Status Dashboard](/.github/img/contributing/status-of-cache.png)
From the existing functionality, we can see that this is a string message that is received as part of a `GET` request from the server.
So to define this, we must first add a handler for the Published Status called `published-status.handlers.ts` within the mocks/domains folder. In this file we will have code that looks like the following:
```typescript
const { rest } = window.MockServiceWorker;
import { umbracoPath } from "@umbraco-cms/backoffice/utils";
export const handlers = [
rest.get(umbracoPath("/published-cache/status"), (_req, res, ctx) => {
return res(
// Respond with a 200 status code
ctx.status(200),
ctx.json<string>(
"Database cache is ok. ContentStore contains 1 item and has 1 generation and 0 snapshot. MediaStore contains 5 items and has 1 generation and 0 snapshot."
)
);
}),
];
```
This is defining the `GET` path that we will call through the resource: `/published-cache/status`
It returns a `200 OK` response and a string value with the current “status” of the published cache for us to use within the element
An example `POST` is similar. Lets take the “Refresh status” button as an example:
![Published Status Dashboard](/.github/img/contributing/refresh-status.png)
From our existing functionality, we can see that this makes a `POST` call to the server to prompt a reload of the published cache. So we would add a new endpoint to the mock handler that would look like:
```typescript
rest.post(umbracoPath('/published-cache/reload'), async (_req, res, ctx) => {
return res(
// Simulate a 1 second delay for the benefit of the UI
ctx.delay(1000)
// Respond with a 201 status code
ctx.status(201)
);
})
```
Which is defining a new `POST` endpoint that we can add to the core API fetcher using the path `/published-cache/reload`.
This call returns a simple `OK` status code and no other object.
## Storybook stories
We try to make good Storybook stories for new components, which is a nice way to work with a component in an isolated state. Imagine you are working with a dialog on page 3 and have to navigate back to that every time you make a change - this is now eliminated with Storybook as you can just make a story that displays that step. Storybook can only show one component at a time, so it also helps us to isolate view logic into more and smaller components, which in turn are more testable.
In-depth: [https://storybook.js.org/docs/web-components/get-started/introduction](https://storybook.js.org/docs/web-components/get-started/introduction)
Reference: [https://ambitious-stone-0033b3603.1.azurestaticapps.net/](https://ambitious-stone-0033b3603.1.azurestaticapps.net/)
- Locally: `npm run storybook`
For Umbraco UI stories, please navigate to [https://uui.umbraco.com/](https://uui.umbraco.com/)
## Testing
There are two testing tools on the backoffice: unit testing and end-to-end testing.
### Unit testing
We are using a tool called Web Test Runner which spins up a bunch of browsers using Playwright with the well-known jasmine/chai syntax. It is expected that any new component/element has a test file named “&lt;component>.test.ts”. It will automatically be picked up and there are a set of standard tests we apply to all components, which checks that they are registered correctly and they pass accessibility testing through Axe.
Working with playwright: [https://playwright.dev/docs/intro](https://playwright.dev/docs/intro)
## Putting it all together
When we are finished with the dashboard we will hopefully have something akin to this [real-world example of the actual dashboard that was migrated](https://github.com/umbraco/Umbraco.CMS.Backoffice/tree/main/src/backoffice/settings/dashboards/published-status).
Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

+6 -6
View File
@@ -13,9 +13,13 @@ changelog:
- title: 💥 Breaking Changes
labels:
- category/breaking
- title: 🐛 Bug Fixes
labels:
- type/bug
- category/bug
- type/improvement
- title: 📄 Documentation
labels:
- documentation
- category/documentation
- title: 🏠 Internal
labels:
@@ -25,14 +29,10 @@ changelog:
- dependencies
- title: 🌈 A11Y
labels:
- accessibility
- category/accessibility
- title: 🚀 New Features
labels:
- type/feature
- category/feature
- type/enhancement
- category/enhancement
- title: 🐛 Bug Fixes
- title: Other Changes
labels:
- '*'
+29 -38
View File
@@ -3,26 +3,20 @@ name: "Code scanning - action"
on:
push:
branches:
- "*/dev"
- "*/contrib"
- "contrib"
- "release/*"
- '*/dev'
- '*/contrib'
pull_request:
# The branches below must be a subset of the branches above
branches:
- "*/dev"
- "*/contrib"
- "contrib"
- "release/*"
schedule:
- cron: "33 2 * * 1"
- '*/dev'
- '*/contrib'
permissions:
contents: read
env:
dotnetVersion: 9.x
dotnetIncludePreviewVersions: "preview"
dotnetVersion: 8.x
dotnetIncludePreviewVersions: true
solution: umbraco.sln
buildConfiguration: SkipTests
DOTNET_NOLOGO: true
@@ -33,39 +27,36 @@ env:
jobs:
CodeQL-Build:
name: Analyze (${{ matrix.language }})
permissions:
actions: read # for github/codeql-action/init to get workflow details
contents: read # for actions/checkout to fetch code
security-events: write # for github/codeql-action/analyze to upload SARIF results
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
include:
- language: csharp
build-mode: none
- language: javascript-typescript
build-mode: none
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Checkout
uses: actions/checkout@v3
with:
fetch-depth: 0
submodules: true
# We use the setup-dotnet action to set up .NET Core, otherwise the CodeQL CLI will not work with preview versions.
- name: Setup .NET from global.json
uses: actions/setup-dotnet@v4
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v2
with:
config-file: ./.github/config/codeql-config.yml
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v3
with:
languages: ${{ matrix.language }}
build-mode: ${{ matrix.build-mode }}
config-file: ./.github/config/codeql-config.yml
- name: Use .NET ${{ env.dotnetVersion }}
uses: actions/setup-dotnet@v2
with:
dotnet-version: ${{ env.dotnetVersion }}
include-prerelease: ${{ env.dotnetIncludePreviewVersions }}
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v3
with:
category: "/language:${{matrix.language}}"
- name: Run dotnet restore
run: dotnet restore ${{ env.solution }}
- name: Run dotnet build
run: dotnet build ${{ env.solution }} --configuration ${{ env.buildConfiguration }} --no-restore -p:ContinuousIntegrationBuild=true
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v2
-65
View File
@@ -1,65 +0,0 @@
name: Test Backoffice
on:
push:
branches:
- contrib
- release/*
- v*/dev
paths:
- src/Umbraco.Web.UI.Client/**
pull_request:
branches:
- contrib
- release/*
- v*/dev
paths:
- src/Umbraco.Web.UI.Client/**
# Allows GitHub to use this workflow to validate the merge queue
merge_group:
# Allows you to run this workflow manually from the Actions tab
workflow_dispatch:
env:
NODE_OPTIONS: --max_old_space_size=16384
jobs:
build:
runs-on: ubuntu-latest
defaults:
run:
working-directory: src/Umbraco.Web.UI.Client
steps:
- uses: actions/checkout@v4
- name: Use Node.js
uses: actions/setup-node@v4
with:
node-version-file: src/Umbraco.Web.UI.Client/.nvmrc
cache: npm
cache-dependency-path: ./src/Umbraco.Web.UI.Client/package-lock.json
- run: npm ci --no-audit --no-fund --prefer-offline
- name: Check for circular dependencies
run: node devops/circular/index.js src
- run: npm run lint:errors
- run: npm run build:for:cms
- run: npm run check:paths
- run: npm run generate:jsonschema:dist
test:
runs-on: ubuntu-latest
defaults:
run:
working-directory: src/Umbraco.Web.UI.Client
steps:
- uses: actions/checkout@v4
- name: Use Node.js
uses: actions/setup-node@v4
with:
node-version-file: src/Umbraco.Web.UI.Client/.nvmrc
cache: npm
cache-dependency-path: ./src/Umbraco.Web.UI.Client/package-lock.json
- run: npm ci --no-audit --no-fund --prefer-offline
- run: npx playwright install --with-deps
- run: npm test
+7
View File
@@ -47,6 +47,9 @@ NDependOut/
QueryResult.htm
tools/docfx/
# Ignore rule for clearing out Belle (avoid rebuilding all the time)
preserve.belle
# csharp-docs
/build/csharp-docs/api/
/build/csharp-docs/_site/
@@ -70,6 +73,10 @@ tools/docfx/
/src/Umbraco.Cms.StaticAssets/wwwroot/umbraco/login
# Environment specific data
/src/Umbraco.Web.UI.Client/[Bb]uild/
/src/Umbraco.Web.UI.Client/[Bb]uild/[Bb]elle/
/src/Umbraco.Web.UI.Client/src/[Ll]ess/*.css
/src/Umbraco.Web.UI.Client/TESTS-*.xml
/src/Umbraco.Web.UI/wwwroot/[Mm]edia/
/src/Umbraco.Web.UI/App_Code/
/src/Umbraco.Web.UI/App_Plugins/
+3
View File
@@ -0,0 +1,3 @@
[submodule "src/Umbraco.Web.UI.Client"]
path = src/Umbraco.Web.UI.Client
url = https://github.com/umbraco/Umbraco.CMS.Backoffice.git
+33 -121
View File
@@ -1,123 +1,35 @@
{
"version": "0.2.0",
"compounds": [
{
"name": "Backoffice Launch (Vite + .NET Core)",
"configurations": [
"Backoffice Launch Vite (Chrome)",
".NET Core Serve with External Auth (web)"
],
"stopAll": true,
"presentation": {
"group": "1"
}
}
],
"configurations": [
{
"name": "Backoffice Launch Vite (Chrome)",
"request": "launch",
"env": {
"VITE_UMBRACO_USE_MSW": "${input:AskForMockServer}"
},
"runtimeExecutable": "npx",
"runtimeArgs": ["vite"],
"type": "node",
"cwd": "${workspaceFolder}/src/Umbraco.Web.UI.Client",
"skipFiles": ["<node_internals>/**", "node_modules/**"],
"smartStep": true,
"autoAttachChildProcesses": true,
"serverReadyAction": {
"killOnServerStop": true,
"action": "debugWithChrome",
"pattern": "Local: http://localhost:([0-9]+)",
"uriFormat": "http://localhost:%s",
"webRoot": "${workspaceFolder}/src/Umbraco.Web.UI.Client"
},
"presentation": {
"group": "2"
}
},
{
"name": "Backoffice Attach Vite (Chrome)",
"request": "launch",
"type": "chrome",
"smartStep": true,
"url": "http://localhost:5173/",
"skipFiles": ["<node_internals>/**", "node_modules/**"],
"webRoot": "${workspaceFolder}/src/Umbraco.Web.UI.Client",
"presentation": {
"group": "2"
}
},
{
// Use IntelliSense to find out which attributes exist for C# debugging
// Use hover for the description of the existing attributes
// For further information visit https://github.com/OmniSharp/omnisharp-vscode/blob/master/debugger-launchjson.md
"name": ".NET Core Launch (web)",
"type": "coreclr",
"request": "launch",
"program": "dotnet",
"args": ["run"],
"cwd": "${workspaceFolder}/src/Umbraco.Web.UI",
"stopAtEntry": false,
"requireExactSource": false,
// Enable launching a web browser when ASP.NET Core starts. For more information: https://aka.ms/VSCode-CS-LaunchJson-WebBrowser
"serverReadyAction": {
"action": "openExternally",
"pattern": "\\\\bNow listening on:\\\\s+(https?://\\\\S+)"
},
"env": {
"ASPNETCORE_ENVIRONMENT": "Development"
},
"sourceFileMap": {
"/Views": "${workspaceFolder}/Umbraco.Web.UI/Views"
},
"presentation": {
"group": "3"
}
},
{
"name": ".NET Core Attach",
"type": "coreclr",
"request": "attach",
"processId": "${command:pickProcess}",
"presentation": {
"group": "3"
}
},
{
"name": ".NET Core Serve with External Auth (web)",
"type": "coreclr",
"request": "launch",
"program": "dotnet",
"args": ["run"],
"cwd": "${workspaceFolder}/src/Umbraco.Web.UI",
"stopAtEntry": false,
"requireExactSource": false,
"checkForDevCert": true,
"env": {
"ASPNETCORE_ENVIRONMENT": "Development",
"ASPNETCORE_URLS": "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"
},
"sourceFileMap": {
"/Views": "${workspaceFolder}/Umbraco.Web.UI/Views"
},
"presentation": {
"group": "3"
}
}
],
"inputs": [
{
"id": "AskForMockServer",
"type": "promptString",
"description": "Use Mock Service Worker (MSW) for Backoffice API calls (off requires a running server)?",
"default": "off"
}
]
"version": "0.2.0",
"configurations": [
{
// Use IntelliSense to find out which attributes exist for C# debugging
// Use hover for the description of the existing attributes
// For further information visit https://github.com/OmniSharp/omnisharp-vscode/blob/master/debugger-launchjson.md
"name": ".NET Core Launch (web)",
"type": "coreclr",
"request": "launch",
"program": "dotnet",
"args": ["run"],
"cwd": "${workspaceFolder}/src/Umbraco.Web.UI",
"stopAtEntry": false,
"requireExactSource": false,
// Enable launching a web browser when ASP.NET Core starts. For more information: https://aka.ms/VSCode-CS-LaunchJson-WebBrowser
"serverReadyAction": {
"action": "openExternally",
"pattern": "\\\\bNow listening on:\\\\s+(https?://\\\\S+)"
},
"env": {
"ASPNETCORE_ENVIRONMENT": "Development"
},
"sourceFileMap": {
"/Views": "${workspaceFolder}/Views"
}
},
{
"name": ".NET Core Attach",
"type": "coreclr",
"request": "attach",
"processId": "${command:pickProcess}"
}
]
}
+7 -3
View File
@@ -27,9 +27,11 @@
"promptOnClose": true,
"group": "build",
"type": "npm",
"script": "build:for:cms",
"script": "build",
"path": "src/Umbraco.Web.UI.Client/",
"problemMatcher": []
"problemMatcher": [
"$gulp-tsc"
]
},
{
"label": "Client Watch",
@@ -39,7 +41,9 @@
"type": "npm",
"script": "dev",
"path": "src/Umbraco.Web.UI.Client/",
"problemMatcher": []
"problemMatcher": [
"$gulp-tsc"
]
},
{
"label": "Dotnet build",
+3 -12
View File
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<Project>
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<TargetFramework>net8.0</TargetFramework>
<Company>Umbraco HQ</Company>
<Authors>Umbraco</Authors>
<Copyright>Copyright © Umbraco $([System.DateTime]::Today.ToString('yyyy'))</Copyright>
@@ -14,21 +14,12 @@
<NeutralLanguage>en-US</NeutralLanguage>
<Nullable>enable</Nullable>
<WarningsAsErrors>nullable</WarningsAsErrors>
<TreatWarningsAsErrors>false</TreatWarningsAsErrors>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<ImplicitUsings>enable</ImplicitUsings>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<WarnOnPackingNonPackableProject>false</WarnOnPackingNonPackableProject>
</PropertyGroup>
<PropertyGroup>
<!--
TODO: Fix and remove overrides:
[NU5104] Warning As Error: A stable release of a package should not have a prerelease dependency. Either modify the version spec of dependency
-->
<NoWarn>$(NoWarn),NU5104,SA1309</NoWarn>
<WarningsNotAsErrors>$(WarningsNotAsErrors),NU5104,SA1600</WarningsNotAsErrors>
</PropertyGroup>
<!-- SourceLink -->
<PropertyGroup>
<PublishRepositoryUrl>true</PublishRepositoryUrl>
@@ -40,7 +31,7 @@
<PropertyGroup>
<GenerateCompatibilitySuppressionFile>false</GenerateCompatibilitySuppressionFile>
<EnablePackageValidation>true</EnablePackageValidation>
<PackageValidationBaselineVersion>15.0.0</PackageValidationBaselineVersion>
<PackageValidationBaselineVersion>14.0.0</PackageValidationBaselineVersion>
<EnableStrictModeForCompatibleFrameworksInPackage>true</EnableStrictModeForCompatibleFrameworksInPackage>
<EnableStrictModeForCompatibleTfms>true</EnableStrictModeForCompatibleTfms>
</PropertyGroup>
+43 -51
View File
@@ -5,35 +5,35 @@
</PropertyGroup>
<!-- Global packages (private, build-time packages for all projects) -->
<ItemGroup>
<GlobalPackageReference Include="Nerdbank.GitVersioning" Version="3.6.146" />
<GlobalPackageReference Include="Nerdbank.GitVersioning" Version="3.6.139" />
<GlobalPackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.556" />
<GlobalPackageReference Include="Umbraco.Code" Version="2.2.0" />
<GlobalPackageReference Include="Umbraco.GitVersioning.Extensions" Version="0.2.0" />
</ItemGroup>
<!-- Microsoft packages -->
<ItemGroup>
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation" Version="9.0.0" />
<PackageVersion Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="8.0.11" />
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation" Version="8.0.11" />
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="4.10.0" />
<PackageVersion Include="Microsoft.Data.Sqlite" Version="9.0.0" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="9.0.0" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.SqlServer" Version="9.0.0" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.10" />
<PackageVersion Include="Microsoft.Extensions.Caching.Abstractions" Version="9.0.0" />
<PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="9.0.0" />
<PackageVersion Include="Microsoft.Extensions.Configuration.Abstractions" Version="9.0.0" />
<PackageVersion Include="Microsoft.Extensions.Configuration.Json" Version="9.0.0" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="9.0.0" />
<PackageVersion Include="Microsoft.Extensions.FileProviders.Embedded" Version="9.0.0" />
<PackageVersion Include="Microsoft.Extensions.FileProviders.Physical" Version="9.0.0" />
<PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="9.0.0" />
<PackageVersion Include="Microsoft.Extensions.Http" Version="9.0.0" />
<PackageVersion Include="Microsoft.Extensions.Identity.Core" Version="9.0.0" />
<PackageVersion Include="Microsoft.Extensions.Identity.Stores" Version="9.0.0" />
<PackageVersion Include="Microsoft.Extensions.Logging" Version="9.0.0" />
<PackageVersion Include="Microsoft.Extensions.Options" Version="9.0.0" />
<PackageVersion Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="9.0.0" />
<PackageVersion Include="Microsoft.Extensions.Options.DataAnnotations" Version="9.0.0" />
<PackageVersion Include="Microsoft.Extensions.Caching.Hybrid" Version="9.0.0-preview.9.24556.5" />
<PackageVersion Include="Microsoft.Data.Sqlite" Version="8.0.11" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.11" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.SqlServer" Version="8.0.11" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.11" />
<PackageVersion Include="Microsoft.Extensions.Caching.Abstractions" Version="8.0.0" />
<PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="8.0.1" />
<PackageVersion Include="Microsoft.Extensions.Configuration.Abstractions" Version="8.0.0" />
<PackageVersion Include="Microsoft.Extensions.Configuration.Json" Version="8.0.0" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="8.0.1" />
<PackageVersion Include="Microsoft.Extensions.FileProviders.Embedded" Version="8.0.11" />
<PackageVersion Include="Microsoft.Extensions.FileProviders.Physical" Version="8.0.0" />
<PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="8.0.0" />
<PackageVersion Include="Microsoft.Extensions.Http" Version="8.0.1" />
<PackageVersion Include="Microsoft.Extensions.Identity.Core" Version="8.0.11" />
<PackageVersion Include="Microsoft.Extensions.Identity.Stores" Version="8.0.11" />
<PackageVersion Include="Microsoft.Extensions.Logging" Version="8.0.1" />
<PackageVersion Include="Microsoft.Extensions.Options" Version="8.0.2" />
<PackageVersion Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="8.0.0" />
<PackageVersion Include="Microsoft.Extensions.Options.DataAnnotations" Version="8.0.0" />
</ItemGroup>
<!-- Umbraco packages -->
<ItemGroup>
@@ -45,7 +45,7 @@
<PackageVersion Include="Asp.Versioning.Mvc" Version="8.1.0" />
<PackageVersion Include="Asp.Versioning.Mvc.ApiExplorer" Version="8.1.0" />
<PackageVersion Include="Dazinator.Extensions.FileProviders" Version="2.0.0" />
<PackageVersion Include="Examine" Version="3.6.0" />
<PackageVersion Include="Examine" Version="3.5.0" />
<PackageVersion Include="Examine.Core" Version="3.5.0" />
<PackageVersion Include="HtmlAgilityPack" Version="1.11.71" />
<PackageVersion Include="JsonPatch.Net" Version="3.1.1" />
@@ -58,46 +58,38 @@
<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.0.0-preview3.24551.41" />
<PackageVersion Include="OpenIddict.AspNetCore" Version="6.0.0-preview3.24551.41" />
<PackageVersion Include="OpenIddict.EntityFrameworkCore" Version="6.0.0-preview3.24551.41" />
<PackageVersion Include="Serilog" Version="4.1.0" />
<PackageVersion Include="OpenIddict.Abstractions" Version="5.7.0" />
<PackageVersion Include="OpenIddict.AspNetCore" Version="5.7.0" />
<PackageVersion Include="OpenIddict.EntityFrameworkCore" Version="5.7.0" />
<PackageVersion Include="Serilog" Version="3.1.1" />
<PackageVersion Include="Serilog.AspNetCore" Version="8.0.3" />
<PackageVersion Include="Serilog.Enrichers.Process" Version="3.0.0" />
<PackageVersion Include="Serilog.Enrichers.Thread" Version="4.0.0" />
<PackageVersion Include="Serilog.Expressions" Version="5.0.0" />
<PackageVersion Include="Serilog.Enrichers.Process" Version="2.0.2" />
<PackageVersion Include="Serilog.Enrichers.Thread" Version="3.1.0" />
<PackageVersion Include="Serilog.Expressions" Version="4.0.0" />
<PackageVersion Include="Serilog.Extensions.Hosting" Version="8.0.0" />
<PackageVersion Include="Serilog.Formatting.Compact" Version="3.0.0" />
<PackageVersion Include="Serilog.Formatting.Compact.Reader" Version="4.0.0" />
<PackageVersion Include="Serilog.Formatting.Compact" Version="2.0.0" />
<PackageVersion Include="Serilog.Formatting.Compact.Reader" Version="3.0.0" />
<PackageVersion Include="Serilog.Settings.Configuration" Version="8.0.4" />
<PackageVersion Include="Serilog.Sinks.Async" Version="2.1.0" />
<PackageVersion Include="Serilog.Sinks.File" Version="6.0.0" />
<PackageVersion Include="Serilog.Sinks.Map" Version="2.0.0" />
<PackageVersion Include="Serilog.Sinks.Async" Version="1.5.0" />
<PackageVersion Include="Serilog.Sinks.File" Version="5.0.0" />
<PackageVersion Include="Serilog.Sinks.Map" Version="1.0.2" />
<PackageVersion Include="SixLabors.ImageSharp" Version="3.1.7" />
<PackageVersion Include="SixLabors.ImageSharp.Web" Version="3.1.3" />
<PackageVersion Include="Swashbuckle.AspNetCore" Version="7.1.0" />
<PackageVersion Include="Swashbuckle.AspNetCore" Version="6.9.0" />
</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 -->
<!-- Both Microsoft.EntityFrameworkCore.SqlServer and NPoco.SqlServer bring in a vulnerable version of Azure.Identity -->
<PackageVersion Include="Azure.Identity" Version="1.13.1" />
<!-- Microsoft.EntityFrameworkCore.SqlServer brings in a vulnerable version of System.Runtime.Caching -->
<PackageVersion Include="System.Runtime.Caching" Version="9.0.0" />
<!-- 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.0" />
<!-- Dazinator.Extensions.FileProviders and MiniProfiler.AspNetCore.Mvc brings in a vulnerable version of System.Text.RegularExpressions -->
<PackageVersion Include="System.Security.Cryptography.Xml" Version="8.0.2" />
<!-- Both Dazinator.Extensions.FileProviders and MiniProfiler.AspNetCore.Mvc bring 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.2.1" />
<!-- 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.0" />
<!-- NPoco.SqlServer brings in a vulnerable version of Microsoft.Data.SqlClient -->
<PackageVersion Include="Microsoft.Data.SqlClient" Version="5.2.2" />
<!-- Examine.Lucene brings in a vulnerable version of Lucene.Net.Replicator -->
<!-- Both OpenIddict.AspNetCore, Npoco.SqlServer and Microsoft.EntityFrameworkCore.SqlServer bring in a vulnerable version of Microsoft.IdentityModel.JsonWebTokens -->
<PackageVersion Include="Microsoft.IdentityModel.JsonWebTokens" Version="7.7.1" />
<!-- Examine.Lucene bring in a vulnerable version of Lucene.Net.Replicator -->
<PackageVersion Include="Lucene.Net.Replicator" Version="4.8.0-beta00017" />
</ItemGroup>
</Project>
</Project>
+93 -182
View File
@@ -36,21 +36,21 @@ parameters:
- name: integrationNonReleaseTestFilter
displayName: TestFilter used for non-release type builds
type: string
default: "--filter TestCategory!=LongRunning&TestCategory!=NonCritical"
default: '--filter TestCategory!=LongRunning&TestCategory!=NonCritical'
- name: integrationReleaseTestFilter
displayName: TestFilter used for release type builds
type: string
default: " "
default: ' '
- name: nonWindowsIntegrationNonReleaseTestFilter
displayName: TestFilter used for non-release type builds on non Windows agents
type: string
default: "--filter TestCategory!=LongRunning&TestCategory!=NonCritical"
default: '--filter TestCategory!=LongRunning&TestCategory!=NonCritical'
- name: nonWindowsIntegrationReleaseTestFilter
displayName: TestFilter used for release type builds on non Windows agents
type: string
default: " "
default: ' '
- name: isNightly
displayName: "Is nightly build (used for MyGet feed)"
displayName: 'Is nightly build (used for MyGet feed)'
type: boolean
default: false
@@ -75,17 +75,24 @@ stages:
- job: A
displayName: Build Umbraco CMS
pool:
vmImage: "windows-latest"
vmImage: 'windows-latest'
steps:
- checkout: self
submodules: false
lfs: false,
fetchDepth: 500
- template: templates/backoffice-install.yml
submodules: true
- task: UseDotNet@2
displayName: Use .NET SDK from global.json
inputs:
useGlobalJson: true
- template: templates/backoffice-install.yml
- script: npm run build:for:cms
displayName: Run build (Bellissima)
workingDirectory: src/Umbraco.Web.UI.Client
- script: npm ci --no-fund --no-audit --prefer-offline
displayName: Run npm ci (Login)
workingDirectory: src/Umbraco.Web.UI.Login
- script: npm run build
displayName: Run npm build (Login)
workingDirectory: src/Umbraco.Web.UI.Login
- task: DotNetCoreCLI@2
displayName: Run dotnet restore
inputs:
@@ -97,7 +104,7 @@ stages:
inputs:
command: build
projects: $(solution)
arguments: "--configuration $(buildConfiguration) --no-restore --property:ContinuousIntegrationBuild=true --property:GeneratePackageOnBuild=true --property:PackageOutputPath=$(Build.ArtifactStagingDirectory)/nupkg"
arguments: '--configuration $(buildConfiguration) --no-restore --property:ContinuousIntegrationBuild=true --property:GeneratePackageOnBuild=true --property:PackageOutputPath=$(Build.ArtifactStagingDirectory)/nupkg'
- task: PublishPipelineArtifact@1
displayName: Publish nupkg
inputs:
@@ -112,12 +119,10 @@ stages:
- job: B
displayName: Build Bellissima Package
pool:
vmImage: "ubuntu-latest"
vmImage: 'ubuntu-latest'
steps:
- checkout: self
submodules: false
lfs: false,
fetchDepth: 500
submodules: true
- template: templates/backoffice-install.yml
- script: npm run build:for:npm
displayName: Run build:for:npm
@@ -147,7 +152,7 @@ stages:
- job:
displayName: Build C# API Reference
pool:
vmImage: "windows-latest"
vmImage: 'windows-latest'
steps:
- task: UseDotNet@2
displayName: Use .NET SDK from global.json
@@ -196,15 +201,12 @@ stages:
- job:
displayName: Build js API Reference
pool:
vmImage: "ubuntu-latest"
vmImage: 'ubuntu-latest'
variables:
BASE_PATH: /v$(umbracoMajorVersion)/ui
steps:
- checkout: self
submodules: false
lfs: false,
fetchDepth: 1
fetchFilter: tree:0
submodules: true
- template: templates/backoffice-install.yml
- script: npm run storybook:build
displayName: Build Storybook
@@ -253,19 +255,14 @@ stages:
strategy:
matrix:
Windows:
vmImage: "windows-latest"
vmImage: 'windows-latest'
Linux:
vmImage: "ubuntu-latest"
vmImage: 'ubuntu-latest'
macOS:
vmImage: "macOS-latest"
vmImage: 'macOS-latest'
pool:
vmImage: $(vmImage)
steps:
- checkout: self
submodules: false
lfs: false,
fetchDepth: 1
fetchFilter: tree:0
- task: DownloadPipelineArtifact@2
displayName: Download build artifacts
inputs:
@@ -279,8 +276,8 @@ stages:
displayName: Run dotnet test
inputs:
command: test
projects: "tests/Umbraco.Tests.UnitTests/Umbraco.Tests.UnitTests.csproj"
arguments: "--configuration $(buildConfiguration) --no-build"
projects: 'tests/Umbraco.Tests.UnitTests/Umbraco.Tests.UnitTests.csproj'
arguments: '--configuration $(buildConfiguration) --no-build'
testRunTitle: Unit Tests - $(Agent.OS)
- stage: Integration
@@ -291,47 +288,20 @@ stages:
jobs:
# Integration Tests (SQLite)
- job:
timeoutInMinutes: 180
displayName: Integration Tests (SQLite)
strategy:
matrix:
# Windows:
# vmImage: 'windows-latest'
# 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)"
LinuxPart2Of3:
vmImage: "ubuntu-latest"
# Filter tests that are part of the Umbraco.Infrastructure.Service namespace
testFilter: "(FullyQualifiedName~Umbraco.Infrastructure.Service)"
LinuxPart3Of3:
vmImage: "ubuntu-latest"
# 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)"
macOSPart2Of3:
vmImage: "macOS-latest"
# Filter tests that are part of the Umbraco.Infrastructure.Service namespace
testFilter: "(FullyQualifiedName~Umbraco.Infrastructure.Service)"
macOSPart3Of3:
vmImage: "macOS-latest"
# Filter tests that are not part of the Umbraco.Infrastructure namespace.
testFilter: "(FullyQualifiedName!~Umbraco.Infrastructure)"
Windows:
vmImage: 'windows-latest'
Linux:
vmImage: 'ubuntu-latest'
macOS:
vmImage: 'macOS-latest'
pool:
vmImage: $(vmImage)
variables:
Tests__Database__DatabaseType: "Sqlite"
Tests__Database__DatabaseType: 'Sqlite'
steps:
- checkout: self
submodules: false
lfs: false,
fetchDepth: 1
fetchFilter: tree:0
# Setup test environment
- task: DownloadPipelineArtifact@2
displayName: Download build artifacts
@@ -349,16 +319,17 @@ stages:
displayName: Run dotnet test
inputs:
command: test
projects: "tests/Umbraco.Tests.Integration/Umbraco.Tests.Integration.csproj"
projects: 'tests/Umbraco.Tests.Integration/Umbraco.Tests.Integration.csproj'
testRunTitle: Integration Tests SQLite - $(Agent.OS)
${{ if and(eq(variables['Agent.OS'],'Windows_NT'), or(variables.releaseTestFilter, parameters.forceReleaseTestFilter)) }}:
arguments: '--filter "$(testFilter)" --configuration $(buildConfiguration) --no-build ${{parameters.integrationReleaseTestFilter}}'
arguments: '--configuration $(buildConfiguration) --no-build ${{parameters.integrationReleaseTestFilter}}'
${{ elseif eq(variables['Agent.OS'],'Windows_NT') }}:
arguments: '--filter "$(testFilter)" --configuration $(buildConfiguration) --no-build ${{parameters.integrationNonReleaseTestFilter}}'
arguments: '--configuration $(buildConfiguration) --no-build ${{parameters.integrationNonReleaseTestFilter}}'
${{ elseif or(variables.releaseTestFilter, parameters.forceReleaseTestFilter) }}:
arguments: '--filter "$(testFilter)" --configuration $(buildConfiguration) --no-build ${{parameters.nonWindowsIntegrationReleaseTestFilter}}'
arguments: '--configuration $(buildConfiguration) --no-build ${{parameters.nonWindowsIntegrationReleaseTestFilter}}'
${{ else }}:
arguments: '--filter "$(testFilter)" --configuration $(buildConfiguration) --no-build ${{parameters.nonWindowsIntegrationNonReleaseTestFilter}}'
arguments: '--configuration $(buildConfiguration) --no-build ${{parameters.nonWindowsIntegrationNonReleaseTestFilter}}'
# Integration Tests (SQL Server)
- job:
timeoutInMinutes: 180
@@ -367,46 +338,15 @@ stages:
displayName: Integration Tests (SQL Server)
strategy:
matrix:
# 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"
Windows:
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)"
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)"
WindowsPart3Of3:
vmImage: "windows-latest"
Tests__Database__DatabaseType: LocalDb
Tests__Database__SQLServerMasterConnectionString: N/A
# 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"
Linux:
vmImage: 'ubuntu-latest'
SA_PASSWORD: UmbracoIntegration123!
Tests__Database__DatabaseType: SqlServer
Tests__Database__SQLServerMasterConnectionString: "Server=(local);User Id=sa;Password=$(SA_PASSWORD);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)"
LinuxPart2Of3:
vmImage: "ubuntu-latest"
SA_PASSWORD: UmbracoIntegration123!
Tests__Database__DatabaseType: SqlServer
Tests__Database__SQLServerMasterConnectionString: "Server=(local);User Id=sa;Password=$(SA_PASSWORD);TrustServerCertificate=True"
# Filter tests that are part of the Umbraco.Infrastructure.Service namespace
testFilter: "(FullyQualifiedName~Umbraco.Infrastructure.Service)"
LinuxPart3Of3:
vmImage: "ubuntu-latest"
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 namespace. So this will run all tests that are not part of the Umbraco.Infrastructure namespace
testFilter: "(FullyQualifiedName!~Umbraco.Infrastructure)"
Tests__Database__SQLServerMasterConnectionString: 'Server=(local);User Id=sa;Password=$(SA_PASSWORD);TrustServerCertificate=True'
pool:
vmImage: $(vmImage)
steps:
@@ -436,16 +376,16 @@ stages:
displayName: Run dotnet test
inputs:
command: test
projects: "tests/Umbraco.Tests.Integration/Umbraco.Tests.Integration.csproj"
projects: 'tests/Umbraco.Tests.Integration/Umbraco.Tests.Integration.csproj'
testRunTitle: Integration Tests SQL Server - $(Agent.OS)
${{ if and(eq(variables['Agent.OS'],'Windows_NT'), or(variables.releaseTestFilter, parameters.forceReleaseTestFilter)) }}:
arguments: '--filter "$(testFilter)" --configuration $(buildConfiguration) --no-build ${{parameters.integrationReleaseTestFilter}}'
arguments: '--configuration $(buildConfiguration) --no-build ${{parameters.integrationReleaseTestFilter}}'
${{ elseif eq(variables['Agent.OS'],'Windows_NT') }}:
arguments: '--filter "$(testFilter)" --configuration $(buildConfiguration) --no-build ${{parameters.integrationNonReleaseTestFilter}}'
arguments: '--configuration $(buildConfiguration) --no-build ${{parameters.integrationNonReleaseTestFilter}}'
${{ elseif or(variables.releaseTestFilter, parameters.forceReleaseTestFilter) }}:
arguments: '--filter "$(testFilter)" --configuration $(buildConfiguration) --no-build ${{parameters.nonWindowsIntegrationReleaseTestFilter}}'
arguments: '--configuration $(buildConfiguration) --no-build ${{parameters.nonWindowsIntegrationReleaseTestFilter}}'
${{ else }}:
arguments: '--filter "$(testFilter)" --configuration $(buildConfiguration) --no-build ${{parameters.nonWindowsIntegrationNonReleaseTestFilter}}'
arguments: '--configuration $(buildConfiguration) --no-build ${{parameters.nonWindowsIntegrationNonReleaseTestFilter}}'
# Stop SQL Server
- pwsh: docker stop mssql
@@ -490,24 +430,10 @@ stages:
CONNECTIONSTRINGS__UMBRACODBDSN_PROVIDERNAME: Microsoft.Data.Sqlite
strategy:
matrix:
LinuxPart1Of3:
vmImage: "ubuntu-latest"
testCommand: "npm run smokeTest -- --shard=1/3"
LinuxPart2Of3:
vmImage: "ubuntu-latest"
testCommand: "npm run smokeTest -- --shard=2/3"
LinuxPart3Of3:
vmImage: "ubuntu-latest"
testCommand: "npm run smokeTest -- --shard=3/3"
WindowsPart1Of3:
vmImage: "windows-latest"
testCommand: "npm run smokeTest -- --shard=1/3"
WindowsPart2Of3:
vmImage: "windows-latest"
testCommand: "npm run smokeTest -- --shard=2/3"
WindowsPart3Of3:
vmImage: "windows-latest"
testCommand: "npm run smokeTest -- --shard=3/3"
Linux:
vmImage: 'ubuntu-latest'
Windows:
vmImage: 'windows-latest'
pool:
vmImage: $(vmImage)
steps:
@@ -580,6 +506,10 @@ stages:
condition: and(succeeded(), eq(variables['Agent.OS'], 'Windows_NT'))
workingDirectory: $(Agent.BuildDirectory)/app
# Ensures we have the package wait-on installed
- pwsh: npm install wait-on
displayName: Install wait-on package
# Wait for application to start responding to requests
- pwsh: npx wait-on -v --interval 1000 --timeout 120000 $(ASPNETCORE_URLS)
displayName: Wait for application
@@ -591,7 +521,7 @@ stages:
workingDirectory: tests/Umbraco.Tests.AcceptanceTest
# Test
- pwsh: $(testCommand)
- pwsh: npm run smokeTestSqlite --ignore-certificate-errors
displayName: Run Playwright tests
continueOnError: true
workingDirectory: tests/Umbraco.Tests.AcceptanceTest
@@ -612,7 +542,7 @@ stages:
# Copy artifacts
- pwsh: |
if (Test-Path tests/Umbraco.Tests.AcceptanceTest/results/*) {
Copy-Item tests/Umbraco.Tests.AcceptanceTest/results/* $(Build.ArtifactStagingDirectory) -Recurse
Copy-Item tests/Umbraco.Tests.AcceptanceTest/results $(Build.ArtifactStagingDirectory) -Recurse
}
displayName: Copy Playwright results
condition: succeededOrFailed()
@@ -623,7 +553,8 @@ stages:
condition: succeededOrFailed()
inputs:
targetPath: $(Build.ArtifactStagingDirectory)
artifact: "Acceptance Test Results - $(Agent.JobName) - Attempt #$(System.JobAttempt)"
artifact: 'Acceptance Tests - $(Agent.JobName) - Attempt #$(System.JobAttempt)'
- job:
displayName: E2E Tests (SQL Server)
variables:
@@ -632,31 +563,13 @@ stages:
CONNECTIONSTRINGS__UMBRACODBDSN_PROVIDERNAME: Microsoft.Data.SqlClient
strategy:
matrix:
${{ if eq(parameters.sqlServerLinuxAcceptanceTests, True) }}:
LinuxPart1Of3:
testCommand: "npm run smokeTestSqlite -- --shard=1/3"
vmImage: "ubuntu-latest"
${{ if eq(parameters.sqlServerLinuxAcceptanceTests, True) }} :
Linux:
vmImage: 'ubuntu-latest'
SA_PASSWORD: $(UMBRACO__CMS__UNATTENDED__UNATTENDEDUSERPASSWORD)
CONNECTIONSTRINGS__UMBRACODBDSN: "Server=(local);Database=Umbraco;User Id=sa;Password=$(SA_PASSWORD);TrustServerCertificate=True"
LinuxPart2Of3:
testCommand: "npm run smokeTestSqlite -- --shard=2/3"
vmImage: "ubuntu-latest"
SA_PASSWORD: $(UMBRACO__CMS__UNATTENDED__UNATTENDEDUSERPASSWORD)
CONNECTIONSTRINGS__UMBRACODBDSN: "Server=(local);Database=Umbraco;User Id=sa;Password=$(SA_PASSWORD);TrustServerCertificate=True"
LinuxPart3Of3:
testCommand: "npm run smokeTestSqlite -- --shard=3/3"
vmImage: "ubuntu-latest"
SA_PASSWORD: $(UMBRACO__CMS__UNATTENDED__UNATTENDEDUSERPASSWORD)
CONNECTIONSTRINGS__UMBRACODBDSN: "Server=(local);Database=Umbraco;User Id=sa;Password=$(SA_PASSWORD);TrustServerCertificate=True"
WindowsPart1Of3:
vmImage: "windows-latest"
testCommand: "npm run smokeTestSqlite -- --shard=1/3"
WindowsPart2Of3:
vmImage: "windows-latest"
testCommand: "npm run smokeTestSqlite -- --shard=2/3"
WindowsPart3Of3:
vmImage: "windows-latest"
testCommand: "npm run smokeTestSqlite -- --shard=3/3"
CONNECTIONSTRINGS__UMBRACODBDSN: 'Server=(local);Database=Umbraco;User Id=sa;Password=$(SA_PASSWORD);TrustServerCertificate=True'
Windows:
vmImage: 'windows-latest'
pool:
vmImage: $(vmImage)
steps:
@@ -748,7 +661,7 @@ stages:
workingDirectory: tests/Umbraco.Tests.AcceptanceTest
# Test
- pwsh: $(testCommand)
- pwsh: npm run smokeTest --ignore-certificate-errors
displayName: Run Playwright tests
continueOnError: true
workingDirectory: tests/Umbraco.Tests.AcceptanceTest
@@ -778,7 +691,7 @@ stages:
# Copy artifacts
- pwsh: |
if (Test-Path tests/Umbraco.Tests.AcceptanceTest/results/*) {
Copy-Item tests/Umbraco.Tests.AcceptanceTest/results/* $(Build.ArtifactStagingDirectory) -Recurse
Copy-Item tests/Umbraco.Tests.AcceptanceTest/results $(Build.ArtifactStagingDirectory) -Recurse
}
displayName: Copy Playwright results
condition: succeededOrFailed()
@@ -789,7 +702,8 @@ stages:
condition: succeededOrFailed()
inputs:
targetPath: $(Build.ArtifactStagingDirectory)
artifact: "Acceptance Test Results - $(Agent.JobName) - Attempt #$(System.JobAttempt)"
artifact: 'Acceptance Tests - $(Agent.JobName) - Attempt #$(System.JobAttempt)'
###############################################
## Release
###############################################
@@ -813,13 +727,13 @@ stages:
- task: NuGetCommand@2
displayName: NuGet push
inputs:
command: "push"
command: 'push'
packagesToPush: $(Build.ArtifactStagingDirectory)/**/*.nupkg
nuGetFeedType: "external"
nuGetFeedType: 'external'
${{ if eq(parameters.isNightly, true) }}:
publishFeedCredentials: "MyGet - Umbraco Nightly"
publishFeedCredentials: 'MyGet - Umbraco Nightly'
${{ else }}:
publishFeedCredentials: "MyGet - Pre-releases"
publishFeedCredentials: 'MyGet - Pre-releases'
- job:
displayName: Push to pre-release feed (npm)
steps:
@@ -843,8 +757,8 @@ stages:
- task: npmAuthenticate@0
displayName: Authenticate with npm (MyGet)
inputs:
workingFile: "$(Pipeline.Workspace)/npm/.npmrc"
customEndpoint: "MyGet (npm) - Umbracoprereleases, MyGet (npm) - Umbraconightly"
workingFile: '$(Pipeline.Workspace)/npm/.npmrc'
customEndpoint: 'MyGet (npm) - Umbracoprereleases, MyGet (npm) - Umbraconightly'
- bash: |
# Setup temp npm project to load in defaults from the local .npmrc
npm init -y
@@ -874,10 +788,10 @@ stages:
- task: NuGetCommand@2
displayName: NuGet push
inputs:
command: "push"
command: 'push'
packagesToPush: $(Build.ArtifactStagingDirectory)/**/*.nupkg
nuGetFeedType: "external"
publishFeedCredentials: "NuGet - Umbraco.*"
nuGetFeedType: 'external'
publishFeedCredentials: 'NuGet - Umbraco.*'
- stage: Deploy_Npm
displayName: Npm release
@@ -898,7 +812,7 @@ stages:
displayName: Authenticate with npm
inputs:
workingFile: $(Pipeline.Workspace)/npm/.npmrc
customEndpoint: "NPM - Umbraco Backoffice"
customEndpoint: 'NPM - Umbraco Backoffice'
- script: |
# Setup temp npm project to load in defaults from the local .npmrc
npm init -y
@@ -911,7 +825,7 @@ stages:
- stage: Upload_API_Docs
pool:
vmImage: "windows-latest" # Apparently AzureFileCopy is windows only :(
vmImage: 'windows-latest' # Apparently AzureFileCopy is windows only :(
variables:
umbracoMajorVersion: $[ stageDependencies.Build.A.outputs['build.NBGV_VersionMajor'] ]
displayName: Upload API Documentation
@@ -933,15 +847,14 @@ stages:
inputs:
archiveFilePatterns: $(Build.SourcesDirectory)/csharp-docs.zip
destinationFolder: $(Build.ArtifactStagingDirectory)/csharp-docs
overwriteExistingFiles: true
- task: AzureFileCopy@4
displayName: "Copy C# Docs to blob storage"
displayName: 'Copy C# Docs to blob storage'
inputs:
SourcePath: "$(Build.ArtifactStagingDirectory)/csharp-docs/*"
SourcePath: '$(Build.ArtifactStagingDirectory)/csharp-docs/*'
azureSubscription: umbraco-storage
Destination: AzureBlob
storage: umbracoapidocs
ContainerName: "$web"
ContainerName: '$web'
BlobPrefix: v$(umbracoMajorVersion)/csharp
CleanTargetBeforeCopy: true
- job:
@@ -957,15 +870,14 @@ stages:
inputs:
archiveFilePatterns: $(Build.SourcesDirectory)/ui-docs.zip
destinationFolder: $(Build.ArtifactStagingDirectory)/ui-docs
overwriteExistingFiles: true
- task: AzureFileCopy@4
displayName: "Copy Storybook to blob storage"
displayName: 'Copy Storybook to blob storage'
inputs:
SourcePath: "$(Build.ArtifactStagingDirectory)/ui-docs/*"
SourcePath: '$(Build.ArtifactStagingDirectory)/ui-docs/*'
azureSubscription: umbraco-storage
Destination: AzureBlob
storage: umbracoapidocs
ContainerName: "$web"
ContainerName: '$web'
BlobPrefix: v$(umbracoMajorVersion)/ui
CleanTargetBeforeCopy: true
- job:
@@ -981,14 +893,13 @@ stages:
inputs:
archiveFilePatterns: $(Build.SourcesDirectory)/ui-api-docs.zip
destinationFolder: $(Build.ArtifactStagingDirectory)/ui-api-docs
overwriteExistingFiles: true
- task: AzureFileCopy@4
displayName: "Copy UI API Docs to blob storage"
displayName: 'Copy UI API Docs to blob storage'
inputs:
SourcePath: "$(Build.ArtifactStagingDirectory)/ui-api-docs/*"
SourcePath: '$(Build.ArtifactStagingDirectory)/ui-api-docs/*'
azureSubscription: umbraco-storage
Destination: AzureBlob
storage: umbracoapidocs
ContainerName: "$web"
ContainerName: '$web'
BlobPrefix: v$(umbracoMajorVersion)/ui-api
CleanTargetBeforeCopy: true
+33 -54
View File
@@ -3,13 +3,12 @@ name: Nightly_E2E_Test_$(TeamProject)_$(Build.DefinitionName)_$(SourceBranchName
pr: none
trigger: none
schedules:
- cron: '0 0 * * *'
displayName: Daily midnight build
branches:
include:
- v14/dev
- v15/dev
# schedules:
# - cron: '0 0 * * *'
# displayName: Daily midnight build
# branches:
# include:
# - v14/dev
variables:
nodeVersion: 20
@@ -23,6 +22,12 @@ variables:
npm_config_cache: $(Pipeline.Workspace)/.npm_client
NODE_OPTIONS: --max_old_space_size=16384
parameters:
- name: runSmokeTests
displayName: Run the smoke tests
type: boolean
default: false
stages:
###############################################
## Build
@@ -109,24 +114,10 @@ stages:
CONNECTIONSTRINGS__UMBRACODBDSN_PROVIDERNAME: Microsoft.Data.Sqlite
strategy:
matrix:
LinuxPart1Of3:
vmImage: "ubuntu-latest"
testCommand: "npm run test -- --shard=1/3"
LinuxPart2Of3:
vmImage: "ubuntu-latest"
testCommand: "npm run test -- --shard=2/3"
LinuxPart3Of3:
vmImage: "ubuntu-latest"
testCommand: "npm run test -- --shard=3/3"
WindowsPart1Of3:
vmImage: "windows-latest"
testCommand: "npm run test -- --shard=1/3"
WindowsPart2Of3:
vmImage: "windows-latest"
testCommand: "npm run test -- --shard=2/3"
WindowsPart3Of3:
vmImage: "windows-latest"
testCommand: "npm run test -- --shard=3/3"
Linux:
vmImage: 'ubuntu-latest'
Windows:
vmImage: 'windows-latest'
pool:
vmImage: $(vmImage)
steps:
@@ -214,7 +205,10 @@ stages:
workingDirectory: tests/Umbraco.Tests.AcceptanceTest
# Test
- pwsh: $(testCommand)
- ${{ if eq(parameters.runSmokeTests, true) }}:
pwsh: npm run smokeTestSqlite --ignore-certificate-errors
${{ else }}:
pwsh: npm run testSqlite --ignore-certificate-errors
displayName: Run Playwright tests
continueOnError: true
workingDirectory: tests/Umbraco.Tests.AcceptanceTest
@@ -235,7 +229,7 @@ stages:
# Copy artifacts
- pwsh: |
if (Test-Path tests/Umbraco.Tests.AcceptanceTest/results/*) {
Copy-Item tests/Umbraco.Tests.AcceptanceTest/results/* $(Build.ArtifactStagingDirectory) -Recurse
Copy-Item tests/Umbraco.Tests.AcceptanceTest/results $(Build.ArtifactStagingDirectory) -Recurse
}
displayName: Copy Playwright results
condition: succeededOrFailed()
@@ -246,7 +240,7 @@ stages:
condition: succeededOrFailed()
inputs:
targetPath: $(Build.ArtifactStagingDirectory)
artifact: "Acceptance Test Results - $(Agent.JobName) - Attempt #$(System.JobAttempt)"
artifact: 'Acceptance Tests - $(Agent.JobName) - Attempt #$(System.JobAttempt)'
- job:
displayName: E2E Tests (SQL Server)
@@ -257,30 +251,12 @@ stages:
CONNECTIONSTRINGS__UMBRACODBDSN_PROVIDERNAME: Microsoft.Data.SqlClient
strategy:
matrix:
LinuxPart1Of3:
testCommand: "npm run testSqlite -- --shard=1/3"
vmImage: "ubuntu-latest"
Linux:
vmImage: 'ubuntu-latest'
SA_PASSWORD: $(UMBRACO__CMS__UNATTENDED__UNATTENDEDUSERPASSWORD)
CONNECTIONSTRINGS__UMBRACODBDSN: "Server=(local);Database=Umbraco;User Id=sa;Password=$(SA_PASSWORD);TrustServerCertificate=True"
LinuxPart2Of3:
testCommand: "npm run testSqlite -- --shard=2/3"
vmImage: "ubuntu-latest"
SA_PASSWORD: $(UMBRACO__CMS__UNATTENDED__UNATTENDEDUSERPASSWORD)
CONNECTIONSTRINGS__UMBRACODBDSN: "Server=(local);Database=Umbraco;User Id=sa;Password=$(SA_PASSWORD);TrustServerCertificate=True"
LinuxPart3Of3:
testCommand: "npm run testSqlite -- --shard=3/3"
vmImage: "ubuntu-latest"
SA_PASSWORD: $(UMBRACO__CMS__UNATTENDED__UNATTENDEDUSERPASSWORD)
CONNECTIONSTRINGS__UMBRACODBDSN: "Server=(local);Database=Umbraco;User Id=sa;Password=$(SA_PASSWORD);TrustServerCertificate=True"
WindowsPart1Of3:
vmImage: "windows-latest"
testCommand: "npm run testSqlite -- --shard=1/3"
WindowsPart2Of3:
vmImage: "windows-latest"
testCommand: "npm run testSqlite -- --shard=2/3"
WindowsPart3Of3:
vmImage: "windows-latest"
testCommand: "npm run testSqlite -- --shard=3/3"
CONNECTIONSTRINGS__UMBRACODBDSN: 'Server=(local);Database=Umbraco;User Id=sa;Password=$(SA_PASSWORD);TrustServerCertificate=True'
Windows:
vmImage: 'windows-latest'
pool:
vmImage: $(vmImage)
steps:
@@ -376,7 +352,10 @@ stages:
workingDirectory: tests/Umbraco.Tests.AcceptanceTest
# Test
- pwsh: $(testCommand)
- ${{ if eq(parameters.runSmokeTests, true) }}:
pwsh: npm run smokeTest --ignore-certificate-errors
${{ else }}:
pwsh: npm run test --ignore-certificate-errors
displayName: Run Playwright tests
continueOnError: true
workingDirectory: tests/Umbraco.Tests.AcceptanceTest
@@ -406,7 +385,7 @@ stages:
# Copy artifacts
- pwsh: |
if (Test-Path tests/Umbraco.Tests.AcceptanceTest/results/*) {
Copy-Item tests/Umbraco.Tests.AcceptanceTest/results/* $(Build.ArtifactStagingDirectory) -Recurse
Copy-Item tests/Umbraco.Tests.AcceptanceTest/results $(Build.ArtifactStagingDirectory) -Recurse
}
displayName: Copy Playwright results
condition: succeededOrFailed()
@@ -417,4 +396,4 @@ stages:
condition: succeededOrFailed()
inputs:
targetPath: $(Build.ArtifactStagingDirectory)
artifact: "Acceptance Test Results - $(Agent.JobName) - Attempt #$(System.JobAttempt)"
artifact: 'Acceptance Tests - $(Agent.JobName) - Attempt #$(System.JobAttempt)'
+2 -3
View File
@@ -1,7 +1,6 @@
{
"sdk": {
"version": "9.0.100",
"rollForward": "latestFeature",
"allowPrerelease": false
"version": "8.0.300",
"rollForward": "latestFeature"
}
}
@@ -1,5 +1,4 @@
using Asp.Versioning;
using Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.AspNetCore.Mvc.ApiExplorer;
using Microsoft.AspNetCore.Mvc.Controllers;
using Microsoft.Extensions.DependencyInjection;
@@ -7,7 +6,6 @@ using Microsoft.Extensions.Options;
using Microsoft.OpenApi.Models;
using Swashbuckle.AspNetCore.SwaggerGen;
using Umbraco.Cms.Api.Common.OpenApi;
using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Extensions;
namespace Umbraco.Cms.Api.Common.Configuration;
@@ -16,23 +14,22 @@ public class ConfigureUmbracoSwaggerGenOptions : IConfigureOptions<SwaggerGenOpt
{
private readonly IOperationIdSelector _operationIdSelector;
private readonly ISchemaIdSelector _schemaIdSelector;
private readonly ISubTypesSelector _subTypesSelector;
[Obsolete("Use non-obsolete constructor. This will be removed in Umbraco 16.")]
[Obsolete("Use non-obsolete constructor. This will be removed in Umbraco 15.")]
public ConfigureUmbracoSwaggerGenOptions(
IOptions<ApiVersioningOptions> apiVersioningOptions,
IOperationIdSelector operationIdSelector,
ISchemaIdSelector schemaIdSelector)
: this(operationIdSelector, schemaIdSelector)
{
}
public ConfigureUmbracoSwaggerGenOptions(
IOperationIdSelector operationIdSelector,
ISchemaIdSelector schemaIdSelector)
: this(operationIdSelector, schemaIdSelector, StaticServiceProvider.Instance.GetRequiredService<ISubTypesSelector>())
{ }
public ConfigureUmbracoSwaggerGenOptions(
IOperationIdSelector operationIdSelector,
ISchemaIdSelector schemaIdSelector,
ISubTypesSelector subTypesSelector)
{
_operationIdSelector = operationIdSelector;
_schemaIdSelector = schemaIdSelector;
_subTypesSelector = subTypesSelector;
}
public void Configure(SwaggerGenOptions swaggerGenOptions)
@@ -49,25 +46,26 @@ public class ConfigureUmbracoSwaggerGenOptions : IConfigureOptions<SwaggerGenOpt
swaggerGenOptions.CustomOperationIds(description => _operationIdSelector.OperationId(description));
swaggerGenOptions.DocInclusionPredicate((name, api) =>
{
if (api.ActionDescriptor is ControllerActionDescriptor controllerActionDescriptor
&& controllerActionDescriptor.MethodInfo.HasMapToApiAttribute(name))
if (string.IsNullOrWhiteSpace(api.GroupName))
{
return true;
return false;
}
ApiVersionMetadata apiVersionMetadata = api.ActionDescriptor.GetApiVersionMetadata();
return apiVersionMetadata.Name == name
|| (string.IsNullOrEmpty(apiVersionMetadata.Name) && name == DefaultApiConfiguration.ApiName);
if (api.ActionDescriptor is ControllerActionDescriptor controllerActionDescriptor)
{
return controllerActionDescriptor.MethodInfo.HasMapToApiAttribute(name);
}
return false;
});
swaggerGenOptions.TagActionsBy(api => new[] { api.GroupName });
swaggerGenOptions.OrderActionsBy(ActionOrderBy);
swaggerGenOptions.SchemaFilter<EnumSchemaFilter>();
swaggerGenOptions.CustomSchemaIds(_schemaIdSelector.SchemaId);
swaggerGenOptions.SelectSubTypesUsing(_subTypesSelector.SubTypes);
swaggerGenOptions.SupportNonNullableReferenceTypes();
}
// see https://github.com/domaindrivendev/Swashbuckle.AspNetCore#change-operation-sort-order-eg-for-ui-sorting
private static string ActionOrderBy(ApiDescription apiDesc)
=> $"{apiDesc.GroupName}_{apiDesc.ActionDescriptor.AttributeRouteInfo?.Template ?? apiDesc.ActionDescriptor.RouteValues["controller"]}_{(apiDesc.ActionDescriptor.RouteValues.TryGetValue("action", out var action) ? action : null)}_{apiDesc.HttpMethod}";
=> $"{apiDesc.GroupName}_{apiDesc.ActionDescriptor.AttributeRouteInfo?.Template ?? apiDesc.ActionDescriptor.RouteValues["controller"]}_{apiDesc.ActionDescriptor.RouteValues["action"]}_{apiDesc.HttpMethod}";
}
@@ -23,8 +23,6 @@ public static class UmbracoBuilderApiExtensions
builder.Services.AddSingleton<IOperationIdHandler, OperationIdHandler>();
builder.Services.AddSingleton<ISchemaIdSelector, SchemaIdSelector>();
builder.Services.AddSingleton<ISchemaIdHandler, SchemaIdHandler>();
builder.Services.AddSingleton<ISubTypesSelector, SubTypesSelector>();
builder.Services.AddSingleton<ISubTypesHandler, SubTypesHandler>();
builder.Services.Configure<UmbracoPipelineOptions>(options => options.AddFilter(new SwaggerRouteTemplatePipelineFilter("UmbracoApiCommon")));
return builder;
@@ -41,13 +41,13 @@ public static class UmbracoBuilderAuthExtensions
.SetTokenEndpointUris(
Paths.MemberApi.TokenEndpoint.TrimStart(Constants.CharArrays.ForwardSlash),
Paths.BackOfficeApi.TokenEndpoint.TrimStart(Constants.CharArrays.ForwardSlash))
.SetEndSessionEndpointUris(
.SetLogoutEndpointUris(
Paths.MemberApi.LogoutEndpoint.TrimStart(Constants.CharArrays.ForwardSlash),
Paths.BackOfficeApi.LogoutEndpoint.TrimStart(Constants.CharArrays.ForwardSlash))
.SetRevocationEndpointUris(
Paths.MemberApi.RevokeEndpoint.TrimStart(Constants.CharArrays.ForwardSlash),
Paths.BackOfficeApi.RevokeEndpoint.TrimStart(Constants.CharArrays.ForwardSlash))
.SetUserInfoEndpointUris(
.SetUserinfoEndpointUris(
Paths.MemberApi.UserinfoEndpoint.TrimStart(Constants.CharArrays.ForwardSlash));
// Enable authorization code flow with PKCE
@@ -56,16 +56,12 @@ public static class UmbracoBuilderAuthExtensions
.RequireProofKeyForCodeExchange()
.AllowRefreshTokenFlow();
// Enable the client credentials flow.
options.AllowClientCredentialsFlow();
// Register the ASP.NET Core host and configure for custom authentication endpoint.
options
.UseAspNetCore()
.EnableAuthorizationEndpointPassthrough()
.EnableTokenEndpointPassthrough()
.EnableEndSessionEndpointPassthrough()
.EnableUserInfoEndpointPassthrough();
.EnableLogoutEndpointPassthrough()
.EnableUserinfoEndpointPassthrough();
// Enable reference tokens
// - see https://documentation.openiddict.com/configuration/token-storage.html
@@ -1,8 +0,0 @@
namespace Umbraco.Cms.Api.Common.OpenApi;
public interface ISubTypesHandler
{
bool CanHandle(Type type, string documentName);
IEnumerable<Type> Handle(Type type);
}
@@ -1,6 +0,0 @@
namespace Umbraco.Cms.Api.Common.OpenApi;
public interface ISubTypesSelector
{
IEnumerable<Type> SubTypes(Type type);
}
@@ -1,25 +0,0 @@
using Microsoft.OpenApi.Models;
using Swashbuckle.AspNetCore.SwaggerGen;
namespace Umbraco.Cms.Api.Common.OpenApi;
/// <summary>
/// This filter explicitly removes all security schemes from a named OpenAPI document.
/// </summary>
public class RemoveSecuritySchemesDocumentFilter : IDocumentFilter
{
private readonly string _documentName;
public RemoveSecuritySchemesDocumentFilter(string documentName)
=> _documentName = documentName;
public void Apply(OpenApiDocument swaggerDoc, DocumentFilterContext context)
{
if (context.DocumentName != _documentName)
{
return;
}
swaggerDoc.Components.SecuritySchemes.Clear();
}
}
@@ -1,20 +0,0 @@
using Umbraco.Cms.Api.Common.Serialization;
namespace Umbraco.Cms.Api.Common.OpenApi;
public class SubTypesHandler : ISubTypesHandler
{
private readonly IUmbracoJsonTypeInfoResolver _umbracoJsonTypeInfoResolver;
public SubTypesHandler(IUmbracoJsonTypeInfoResolver umbracoJsonTypeInfoResolver)
=> _umbracoJsonTypeInfoResolver = umbracoJsonTypeInfoResolver;
protected virtual bool CanHandle(Type type)
=> type.Namespace?.StartsWith("Umbraco.Cms") is true;
public virtual bool CanHandle(Type type, string documentName)
=> CanHandle(type);
public virtual IEnumerable<Type> Handle(Type type)
=> _umbracoJsonTypeInfoResolver.FindSubTypes(type);
}
@@ -1,60 +0,0 @@
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Options;
using Umbraco.Cms.Api.Common.Serialization;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Configuration.Models;
using Umbraco.Cms.Core.Hosting;
using Umbraco.Extensions;
namespace Umbraco.Cms.Api.Common.OpenApi;
public class SubTypesSelector : ISubTypesSelector
{
private readonly IOptions<GlobalSettings> _settings;
private readonly IHostingEnvironment _hostingEnvironment;
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly IEnumerable<ISubTypesHandler> _subTypeHandlers;
private readonly IUmbracoJsonTypeInfoResolver _umbracoJsonTypeInfoResolver;
public SubTypesSelector(
IOptions<GlobalSettings> settings,
IHostingEnvironment hostingEnvironment,
IHttpContextAccessor httpContextAccessor,
IEnumerable<ISubTypesHandler> subTypeHandlers,
IUmbracoJsonTypeInfoResolver umbracoJsonTypeInfoResolver)
{
_settings = settings;
_hostingEnvironment = hostingEnvironment;
_httpContextAccessor = httpContextAccessor;
_subTypeHandlers = subTypeHandlers;
_umbracoJsonTypeInfoResolver = umbracoJsonTypeInfoResolver;
}
public IEnumerable<Type> SubTypes(Type type)
{
var backOfficePath = _settings.Value.GetBackOfficePath(_hostingEnvironment);
var swaggerPath = $"{backOfficePath}/swagger";
if (_httpContextAccessor.HttpContext?.Request.Path.StartsWithSegments(swaggerPath) ?? false)
{
// Split the path into segments
var segments = _httpContextAccessor.HttpContext.Request.Path.Value!
.Substring(swaggerPath.Length)
.TrimStart(Constants.CharArrays.ForwardSlash)
.Split(Constants.CharArrays.ForwardSlash);
// Extract the document name from the path
var documentName = segments[0];
// Find the first handler that can handle the type / document name combination
ISubTypesHandler? handler = _subTypeHandlers.FirstOrDefault(h => h.CanHandle(type, documentName));
if (handler != null)
{
return handler.Handle(type);
}
}
// Default implementation to maintain backwards compatibility
return _umbracoJsonTypeInfoResolver.FindSubTypes(type);
}
}
@@ -3,7 +3,6 @@
<Title>Umbraco CMS - API Common</Title>
<Description>Contains the bits and pieces that are shared between the Umbraco CMS APIs.</Description>
</PropertyGroup>
<ItemGroup>
<FrameworkReference Include="Microsoft.AspNetCore.App" />
</ItemGroup>
@@ -14,9 +13,10 @@
<PackageReference Include="Swashbuckle.AspNetCore" />
<PackageReference Include="OpenIddict.Abstractions" />
<PackageReference Include="OpenIddict.AspNetCore" />
</ItemGroup>
<ItemGroup>
<!-- Both OpenIddict.AspNetCore, Npoco.SqlServer and Microsoft.EntityFrameworkCore.SqlServer bring in a vulnerable version of Microsoft.IdentityModel.JsonWebTokens -->
<PackageReference Include="Microsoft.IdentityModel.JsonWebTokens"/>
<!-- Take top-level depedendency on OpenIddict.AspNetCore depends on a vulnerable version -->
<PackageReference Include="Microsoft.Extensions.Caching.Memory" />
</ItemGroup>
@@ -24,5 +24,6 @@
<ItemGroup>
<ProjectReference Include="..\Umbraco.Core\Umbraco.Core.csproj" />
<ProjectReference Include="..\Umbraco.Web.Common\Umbraco.Web.Common.csproj" />
</ItemGroup>
</Project>
@@ -1,19 +0,0 @@
using Microsoft.AspNetCore.OutputCaching;
namespace Umbraco.Cms.Api.Delivery.Caching;
internal sealed class NoOutputCachePolicy : IOutputCachePolicy
{
ValueTask IOutputCachePolicy.CacheRequestAsync(OutputCacheContext context, CancellationToken cancellationToken)
{
context.EnableOutputCaching = false;
return ValueTask.CompletedTask;
}
ValueTask IOutputCachePolicy.ServeFromCacheAsync(OutputCacheContext context, CancellationToken cancellationToken)
=> ValueTask.CompletedTask;
ValueTask IOutputCachePolicy.ServeResponseAsync(OutputCacheContext context, CancellationToken cancellationToken)
=> ValueTask.CompletedTask;
}
@@ -21,7 +21,6 @@ public class ConfigureUmbracoDeliveryApiSwaggerGenOptions: IConfigureOptions<Swa
});
swaggerGenOptions.DocumentFilter<MimeTypeDocumentFilter>(DeliveryApiConfiguration.ApiName);
swaggerGenOptions.DocumentFilter<RemoveSecuritySchemesDocumentFilter>(DeliveryApiConfiguration.ApiName);
swaggerGenOptions.OperationFilter<SwaggerContentDocumentationFilter>();
swaggerGenOptions.OperationFilter<SwaggerMediaDocumentationFilter>();
@@ -17,16 +17,33 @@ namespace Umbraco.Cms.Api.Delivery.Configuration;
/// </remarks>
public class ConfigureUmbracoMemberAuthenticationDeliveryApiSwaggerGenOptions : IConfigureOptions<SwaggerGenOptions>
{
private const string AuthSchemeName = "UmbracoMember";
private const string AuthSchemeName = "Umbraco Member";
public void Configure(SwaggerGenOptions options)
{
options.AddSecurityDefinition(
AuthSchemeName,
new OpenApiSecurityScheme
{
In = ParameterLocation.Header,
Name = AuthSchemeName,
Type = SecuritySchemeType.OAuth2,
Description = "Umbraco Member Authentication",
Flows = new OpenApiOAuthFlows
{
AuthorizationCode = new OpenApiOAuthFlow
{
AuthorizationUrl = new Uri(Paths.MemberApi.AuthorizationEndpoint, UriKind.Relative),
TokenUrl = new Uri(Paths.MemberApi.TokenEndpoint, UriKind.Relative)
}
}
});
// add security requirements for content API operations
options.DocumentFilter<DeliveryApiSecurityFilter>();
options.OperationFilter<DeliveryApiSecurityFilter>();
}
private class DeliveryApiSecurityFilter : SwaggerFilterBase<ContentApiControllerBase>, IOperationFilter, IDocumentFilter
private class DeliveryApiSecurityFilter : SwaggerFilterBase<ContentApiControllerBase>, IOperationFilter
{
public void Apply(OpenApiOperation operation, OperationFilterContext context)
{
@@ -53,31 +70,5 @@ public class ConfigureUmbracoMemberAuthenticationDeliveryApiSwaggerGenOptions :
}
};
}
public void Apply(OpenApiDocument swaggerDoc, DocumentFilterContext context)
{
if (context.DocumentName != DeliveryApiConfiguration.ApiName)
{
return;
}
swaggerDoc.Components.SecuritySchemes.Add(
AuthSchemeName,
new OpenApiSecurityScheme
{
In = ParameterLocation.Header,
Name = AuthSchemeName,
Type = SecuritySchemeType.OAuth2,
Description = "Umbraco Member Authentication",
Flows = new OpenApiOAuthFlows
{
AuthorizationCode = new OpenApiOAuthFlow
{
AuthorizationUrl = new Uri(Paths.MemberApi.AuthorizationEndpoint, UriKind.Relative),
TokenUrl = new Uri(Paths.MemberApi.TokenEndpoint, UriKind.Relative)
}
}
});
}
}
}
@@ -1,4 +1,3 @@
using System.Diagnostics;
using Asp.Versioning;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
@@ -47,17 +46,19 @@ public class ByIdContentApiController : ContentApiItemControllerBase
private async Task<IActionResult> HandleRequest(Guid id)
{
IPublishedContent? contentItem = await ApiPublishedContentCache.GetByIdAsync(id).ConfigureAwait(false);
IPublishedContent? contentItem = ApiPublishedContentCache.GetById(id);
if (contentItem is null)
{
return NotFound();
}
IActionResult? deniedAccessResult = await HandleMemberAccessAsync(contentItem, _requestMemberAccessService).ConfigureAwait(false);
IActionResult? deniedAccessResult = await HandleMemberAccessAsync(contentItem, _requestMemberAccessService);
if (deniedAccessResult is not null)
{
return deniedAccessResult;
}
IApiContentResponse? apiContentResponse = ApiContentResponseBuilder.Build(contentItem);
if (apiContentResponse is null)
{
@@ -45,7 +45,7 @@ public class ByIdsContentApiController : ContentApiItemControllerBase
private async Task<IActionResult> HandleRequest(HashSet<Guid> ids)
{
IPublishedContent[] contentItems = (await ApiPublishedContentCache.GetByIdsAsync(ids).ConfigureAwait(false)).ToArray();
IPublishedContent[] contentItems = ApiPublishedContentCache.GetByIds(ids).ToArray();
IActionResult? deniedAccessResult = await HandleMemberAccessAsync(contentItems, _requestMemberAccessService);
if (deniedAccessResult is not null)
@@ -12,10 +12,8 @@ namespace Umbraco.Cms.Api.Delivery.Controllers.Media;
[ApiVersion("2.0")]
public class ByIdMediaApiController : MediaApiControllerBase
{
public ByIdMediaApiController(
IPublishedMediaCache publishedMediaCache,
IApiMediaWithCropsResponseBuilder apiMediaWithCropsResponseBuilder)
: base(publishedMediaCache, apiMediaWithCropsResponseBuilder)
public ByIdMediaApiController(IPublishedSnapshotAccessor publishedSnapshotAccessor, IApiMediaWithCropsResponseBuilder apiMediaWithCropsResponseBuilder)
: base(publishedSnapshotAccessor, apiMediaWithCropsResponseBuilder)
{
}
@@ -13,8 +13,8 @@ namespace Umbraco.Cms.Api.Delivery.Controllers.Media;
[ApiVersion("2.0")]
public class ByIdsMediaApiController : MediaApiControllerBase
{
public ByIdsMediaApiController(IPublishedMediaCache publishedMediaCache, IApiMediaWithCropsResponseBuilder apiMediaWithCropsResponseBuilder)
: base(publishedMediaCache, apiMediaWithCropsResponseBuilder)
public ByIdsMediaApiController(IPublishedSnapshotAccessor publishedSnapshotAccessor, IApiMediaWithCropsResponseBuilder apiMediaWithCropsResponseBuilder)
: base(publishedSnapshotAccessor, apiMediaWithCropsResponseBuilder)
{
}
@@ -16,10 +16,10 @@ public class ByPathMediaApiController : MediaApiControllerBase
private readonly IApiMediaQueryService _apiMediaQueryService;
public ByPathMediaApiController(
IPublishedMediaCache publishedMediaCache,
IPublishedSnapshotAccessor publishedSnapshotAccessor,
IApiMediaWithCropsResponseBuilder apiMediaWithCropsResponseBuilder,
IApiMediaQueryService apiMediaQueryService)
: base(publishedMediaCache, apiMediaWithCropsResponseBuilder)
: base(publishedSnapshotAccessor, apiMediaWithCropsResponseBuilder)
=> _apiMediaQueryService = apiMediaQueryService;
[HttpGet("item/{*path}")]
@@ -20,15 +20,18 @@ namespace Umbraco.Cms.Api.Delivery.Controllers.Media;
public abstract class MediaApiControllerBase : DeliveryApiControllerBase
{
private readonly IApiMediaWithCropsResponseBuilder _apiMediaWithCropsResponseBuilder;
private IPublishedMediaCache _publishedMediaCache;
private readonly IPublishedSnapshotAccessor _publishedSnapshotAccessor;
private IPublishedMediaCache? _publishedMediaCache;
protected MediaApiControllerBase(IPublishedMediaCache publishedMediaCache, IApiMediaWithCropsResponseBuilder apiMediaWithCropsResponseBuilder)
protected MediaApiControllerBase(IPublishedSnapshotAccessor publishedSnapshotAccessor, IApiMediaWithCropsResponseBuilder apiMediaWithCropsResponseBuilder)
{
_publishedMediaCache = publishedMediaCache;
_publishedSnapshotAccessor = publishedSnapshotAccessor;
_apiMediaWithCropsResponseBuilder = apiMediaWithCropsResponseBuilder;
}
protected IPublishedMediaCache PublishedMediaCache => _publishedMediaCache;
protected IPublishedMediaCache PublishedMediaCache => _publishedMediaCache
??= _publishedSnapshotAccessor.GetRequiredPublishedSnapshot().Media
?? throw new InvalidOperationException("Could not obtain the published media cache");
protected IApiMediaWithCropsResponse BuildApiMediaWithCrops(IPublishedContent media)
=> _apiMediaWithCropsResponseBuilder.Build(media);
@@ -21,10 +21,10 @@ public class QueryMediaApiController : MediaApiControllerBase
private readonly IApiMediaQueryService _apiMediaQueryService;
public QueryMediaApiController(
IPublishedMediaCache publishedMediaCache,
IPublishedSnapshotAccessor publishedSnapshotAccessor,
IApiMediaWithCropsResponseBuilder apiMediaWithCropsResponseBuilder,
IApiMediaQueryService apiMediaQueryService)
: base(publishedMediaCache, apiMediaWithCropsResponseBuilder)
: base(publishedSnapshotAccessor, apiMediaWithCropsResponseBuilder)
=> _apiMediaQueryService = apiMediaQueryService;
[HttpGet]
@@ -5,7 +5,6 @@ using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using OpenIddict.Abstractions;
@@ -13,7 +12,6 @@ using OpenIddict.Server.AspNetCore;
using Umbraco.Cms.Api.Delivery.Routing;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Configuration.Models;
using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Core.Security;
using Umbraco.Cms.Web.Common.Security;
using Umbraco.Extensions;
@@ -27,47 +25,22 @@ namespace Umbraco.Cms.Api.Delivery.Controllers.Security;
[ApiExplorerSettings(IgnoreApi = true)]
public class MemberController : DeliveryApiControllerBase
{
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly IMemberSignInManager _memberSignInManager;
private readonly IMemberManager _memberManager;
private readonly IMemberClientCredentialsManager _memberClientCredentialsManager;
private readonly DeliveryApiSettings _deliveryApiSettings;
private readonly ILogger<MemberController> _logger;
[Obsolete("Please use the non-obsolete constructor. Will be removed in V16.")]
public MemberController(
IHttpContextAccessor httpContextAccessor,
IMemberSignInManager memberSignInManager,
IMemberManager memberManager,
IOptions<DeliveryApiSettings> deliveryApiSettings,
ILogger<MemberController> logger)
: this(memberSignInManager, memberManager, StaticServiceProvider.Instance.GetRequiredService<IMemberClientCredentialsManager>(), deliveryApiSettings, logger)
{
}
[Obsolete("Please use the non-obsolete constructor. Will be removed in V16.")]
public MemberController(
IHttpContextAccessor httpContextAccessor,
IMemberSignInManager memberSignInManager,
IMemberManager memberManager,
IMemberClientCredentialsManager memberClientCredentialsManager,
IOptions<DeliveryApiSettings> deliveryApiSettings,
ILogger<MemberController> logger)
: this(memberSignInManager, memberManager, memberClientCredentialsManager, deliveryApiSettings, logger)
{
}
[ActivatorUtilitiesConstructor]
public MemberController(
IMemberSignInManager memberSignInManager,
IMemberManager memberManager,
IMemberClientCredentialsManager memberClientCredentialsManager,
IOptions<DeliveryApiSettings> deliveryApiSettings,
ILogger<MemberController> logger)
{
_httpContextAccessor = httpContextAccessor;
_memberSignInManager = memberSignInManager;
_memberManager = memberManager;
_memberClientCredentialsManager = memberClientCredentialsManager;
_logger = logger;
_deliveryApiSettings = deliveryApiSettings.Value;
}
@@ -76,31 +49,25 @@ public class MemberController : DeliveryApiControllerBase
[MapToApiVersion("1.0")]
public async Task<IActionResult> Authorize()
{
// the Authorize endpoint is not allowed unless authorization code flow is enabled.
if (_deliveryApiSettings.MemberAuthorization?.AuthorizationCodeFlow?.Enabled is not true)
// in principle this is not necessary for now, since the member application has been removed, thus making
// the member client ID invalid for the authentication code flow. However, if we ever add additional flows
// to the API, we should perform this check, so we might as well include it upfront.
if (_deliveryApiSettings.MemberAuthorizationIsEnabled() is false)
{
return BadRequest(new OpenIddictResponse
{
Error = "Not allowed", ErrorDescription = "Member authorization is not allowed."
});
return BadRequest("Member authorization is not allowed.");
}
OpenIddictRequest? request = HttpContext.GetOpenIddictServerRequest();
HttpContext context = _httpContextAccessor.GetRequiredHttpContext();
OpenIddictRequest? request = context.GetOpenIddictServerRequest();
if (request is null)
{
return BadRequest(new OpenIddictResponse
{
Error = "No context found", ErrorDescription = "Unable to obtain context from the current request."
});
return BadRequest("Unable to obtain OpenID data from the current request.");
}
// make sure this endpoint ONLY handles member authentication
if (request.ClientId is not Constants.OAuthClientIds.Member)
{
return BadRequest(new OpenIddictResponse
{
Error = "Invalid 'client ID'", ErrorDescription = "The specified 'client_id' is not valid."
});
return BadRequest("The specified client ID cannot be used here.");
}
return request.IdentityProvider.IsNullOrWhiteSpace()
@@ -108,50 +75,6 @@ public class MemberController : DeliveryApiControllerBase
: await AuthorizeExternal(request);
}
[HttpPost("token")]
[MapToApiVersion("1.0")]
public async Task<IActionResult> Token()
{
OpenIddictRequest? request = HttpContext.GetOpenIddictServerRequest();
if (request is null)
{
return BadRequest(new OpenIddictResponse
{
Error = "No context found", ErrorDescription = "Unable to obtain context from the current request."
});
}
// authorization code flow or refresh token flow?
if ((request.IsAuthorizationCodeGrantType() || request.IsRefreshTokenGrantType()) && _deliveryApiSettings.MemberAuthorization?.AuthorizationCodeFlow?.Enabled is true)
{
// attempt to authorize against the supplied the authorization code
AuthenticateResult authenticateResult = await HttpContext.AuthenticateAsync(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
return authenticateResult is { Succeeded: true, Principal: not null }
? new SignInResult(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme, authenticateResult.Principal)
: BadRequest(new OpenIddictResponse
{
Error = "Authorization failed", ErrorDescription = "The supplied authorization could not be verified."
});
}
// client credentials flow?
if (request.IsClientCredentialsGrantType() && _deliveryApiSettings.MemberAuthorization?.ClientCredentialsFlow?.Enabled is true)
{
// if we get here, the client ID and secret are valid (verified by OpenIddict)
MemberIdentityUser? member = await _memberClientCredentialsManager.FindMemberAsync(request.ClientId!);
return member is not null
? await SignInMember(member, request)
: BadRequest(new OpenIddictResponse
{
Error = "Authorization failed", ErrorDescription = "Invalid 'client_id' or client configuration."
});
}
throw new InvalidOperationException("The requested grant type is not supported.");
}
[HttpGet("signout")]
[MapToApiVersion("1.0")]
public async Task<IActionResult> Signout()
@@ -177,10 +100,7 @@ public class MemberController : DeliveryApiControllerBase
if (member is null)
{
_logger.LogError("The member with username {userName} was successfully authorized, but could not be retrieved by the member manager", userName);
return BadRequest(new OpenIddictResponse
{
Error = "Authorization failed", ErrorDescription = "The member associated with the supplied 'client_id' could not be found."
});
return BadRequest("The member could not be found.");
}
return await SignInMember(member, request);
@@ -208,10 +128,7 @@ public class MemberController : DeliveryApiControllerBase
if (member is null)
{
_logger.LogError("A member was successfully authorized using external authentication, but could not be retrieved by the member manager");
return BadRequest(new OpenIddictResponse
{
Error = "Authorization failed", ErrorDescription = "The member associated with the supplied 'client_id' could not be found."
});
return BadRequest("The member could not be found.");
}
// update member authentication tokens if succeeded
@@ -21,7 +21,6 @@ using Umbraco.Cms.Core.Configuration.Models;
using Umbraco.Cms.Core.DeliveryApi;
using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Core.Notifications;
using Umbraco.Cms.Core.Security;
using Umbraco.Cms.Infrastructure.Security;
using Umbraco.Cms.Web.Common.ApplicationBuilder;
@@ -62,7 +61,6 @@ public static class UmbracoBuilderExtensions
builder.Services.AddTransient<IMemberApplicationManager, MemberApplicationManager>();
builder.Services.AddTransient<IRequestMemberAccessService, RequestMemberAccessService>();
builder.Services.AddTransient<ICurrentMemberClaimsProvider, CurrentMemberClaimsProvider>();
builder.Services.AddScoped<IMemberClientCredentialsManager, MemberClientCredentialsManager>();
builder.Services.ConfigureOptions<ConfigureUmbracoDeliveryApiSwaggerGenOptions>();
builder.AddUmbracoApiOpenApiUI();
@@ -106,7 +104,7 @@ public static class UmbracoBuilderExtensions
builder.Services.AddOutputCache(options =>
{
options.AddBasePolicy(build => build.AddPolicy<NoOutputCachePolicy>());
options.AddBasePolicy(_ => { });
if (outputCacheSettings.ContentDuration.TotalSeconds > 0)
{
@@ -5,7 +5,6 @@ using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Configuration.Models;
using Umbraco.Cms.Core.Events;
using Umbraco.Cms.Core.Notifications;
using Umbraco.Cms.Core.Security;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Infrastructure.Security;
@@ -17,19 +16,16 @@ internal sealed class InitializeMemberApplicationNotificationHandler : INotifica
private readonly ILogger<InitializeMemberApplicationNotificationHandler> _logger;
private readonly DeliveryApiSettings _deliveryApiSettings;
private readonly IServiceScopeFactory _serviceScopeFactory;
private readonly IMemberClientCredentialsManager _memberClientCredentialsManager;
public InitializeMemberApplicationNotificationHandler(
IRuntimeState runtimeState,
IOptions<DeliveryApiSettings> deliveryApiSettings,
ILogger<InitializeMemberApplicationNotificationHandler> logger,
IServiceScopeFactory serviceScopeFactory,
IMemberClientCredentialsManager memberClientCredentialsManager)
IServiceScopeFactory serviceScopeFactory)
{
_runtimeState = runtimeState;
_logger = logger;
_serviceScopeFactory = serviceScopeFactory;
_memberClientCredentialsManager = memberClientCredentialsManager;
_deliveryApiSettings = deliveryApiSettings.Value;
}
@@ -45,12 +41,6 @@ internal sealed class InitializeMemberApplicationNotificationHandler : INotifica
using IServiceScope scope = _serviceScopeFactory.CreateScope();
IMemberApplicationManager memberApplicationManager = scope.ServiceProvider.GetRequiredService<IMemberApplicationManager>();
await HandleMemberApplication(memberApplicationManager, cancellationToken);
await HandleMemberClientCredentialsApplication(memberApplicationManager, cancellationToken);
}
private async Task HandleMemberApplication(IMemberApplicationManager memberApplicationManager, CancellationToken cancellationToken)
{
if (_deliveryApiSettings.MemberAuthorization?.AuthorizationCodeFlow?.Enabled is not true)
{
await memberApplicationManager.DeleteMemberApplicationAsync(cancellationToken);
@@ -76,21 +66,6 @@ internal sealed class InitializeMemberApplicationNotificationHandler : INotifica
cancellationToken);
}
private async Task HandleMemberClientCredentialsApplication(IMemberApplicationManager memberApplicationManager, CancellationToken cancellationToken)
{
if (_deliveryApiSettings.MemberAuthorization?.ClientCredentialsFlow?.Enabled is not true)
{
// disabled
return;
}
IEnumerable<MemberClientCredentials> memberClientCredentials = await _memberClientCredentialsManager.GetAllAsync();
foreach (MemberClientCredentials memberClientCredential in memberClientCredentials)
{
await memberApplicationManager.EnsureMemberClientCredentialsApplicationAsync(memberClientCredential.ClientId, memberClientCredential.ClientSecret, cancellationToken);
}
}
private bool ValidateRedirectUrls(Uri[] redirectUrls)
{
if (redirectUrls.Any() is false)
@@ -1,6 +1,4 @@
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;
@@ -9,45 +7,15 @@ namespace Umbraco.Cms.Api.Delivery.Querying;
public abstract class QueryOptionBase
{
private readonly IPublishedSnapshotAccessor _publishedSnapshotAccessor;
private readonly IRequestRoutingService _requestRoutingService;
private readonly IRequestPreviewService _requestPreviewService;
private readonly IApiDocumentUrlService _apiDocumentUrlService;
private readonly IVariationContextAccessor _variationContextAccessor;
[Obsolete("Please use the non-obsolete constructor. Will be removed in V17.")]
public QueryOptionBase(
IPublishedContentCache publishedContentCache,
IPublishedSnapshotAccessor publishedSnapshotAccessor,
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,
IApiDocumentUrlService apiDocumentUrlService,
IVariationContextAccessor variationContextAccessor)
{
_publishedSnapshotAccessor = publishedSnapshotAccessor;
_requestRoutingService = requestRoutingService;
_requestPreviewService = requestPreviewService;
_apiDocumentUrlService = apiDocumentUrlService;
_variationContextAccessor = variationContextAccessor;
}
protected Guid? GetGuidFromQuery(string queryStringValue)
@@ -62,11 +30,12 @@ public abstract class QueryOptionBase
return id;
}
IPublishedSnapshot publishedSnapshot = _publishedSnapshotAccessor.GetRequiredPublishedSnapshot();
// Check if the passed value is a path of a content item
var contentRoute = _requestRoutingService.GetContentRoute(queryStringValue);
return _apiDocumentUrlService.GetDocumentKeyByRoute(
contentRoute,
_variationContextAccessor.VariationContext?.Culture,
_requestPreviewService.IsPreview());
IPublishedContent? contentItem = publishedSnapshot.Content?.GetByRoute(contentRoute);
return contentItem?.Key;
}
}
@@ -4,74 +4,29 @@ 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;
using Umbraco.Extensions;
namespace Umbraco.Cms.Api.Delivery.Querying.Selectors;
public sealed class AncestorsSelector : QueryOptionBase, ISelectorHandler
{
private readonly IDocumentNavigationQueryService _navigationQueryService;
private const string AncestorsSpecifier = "ancestors:";
private readonly IPublishedSnapshotAccessor _publishedSnapshotAccessor;
private readonly IRequestPreviewService _requestPreviewService;
[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)
IPublishedSnapshotAccessor publishedSnapshotAccessor,
IRequestRoutingService requestRoutingService)
: this(publishedSnapshotAccessor, requestRoutingService, StaticServiceProvider.Instance.GetRequiredService<IRequestPreviewService>())
{
}
[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,
IApiDocumentUrlService apiDocumentUrlService,
IVariationContextAccessor variationContextAccessor,
IDocumentNavigationQueryService navigationQueryService)
: 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)
public AncestorsSelector(IPublishedSnapshotAccessor publishedSnapshotAccessor, IRequestRoutingService requestRoutingService, IRequestPreviewService requestPreviewService)
: base(publishedSnapshotAccessor, requestRoutingService)
{
_publishedSnapshotAccessor = publishedSnapshotAccessor;
_requestPreviewService = requestPreviewService;
}
/// <inheritdoc />
@@ -84,7 +39,7 @@ public sealed class AncestorsSelector : QueryOptionBase, ISelectorHandler
var fieldValue = selector[AncestorsSpecifier.Length..];
Guid? id = GetGuidFromQuery(fieldValue);
if (id is null || _navigationQueryService.TryGetAncestorsKeys(id.Value, out IEnumerable<Guid> ancestorKeys) is false)
if (id is null)
{
// Setting the Value to "" since that would yield no results.
// It won't be appropriate to return null here since if we reached this,
@@ -96,10 +51,27 @@ public sealed class AncestorsSelector : QueryOptionBase, ISelectorHandler
};
}
IPublishedContentCache contentCache = _publishedSnapshotAccessor.GetRequiredPublishedSnapshot()?.Content
?? throw new InvalidOperationException("Could not obtain the content cache");
IPublishedContent? contentItem = contentCache.GetById(_requestPreviewService.IsPreview(), id.Value);
if (contentItem is null)
{
// no such content item, make sure the selector does not yield any results
return new SelectorOption
{
FieldName = AncestorsSelectorIndexer.FieldName,
Values = Array.Empty<string>()
};
}
var ancestorKeys = contentItem.Ancestors().Select(a => a.Key.ToString("D")).ToArray();
return new SelectorOption
{
FieldName = AncestorsSelectorIndexer.FieldName,
Values = ancestorKeys.Select(key => key.ToString("D")).ToArray()
Values = ancestorKeys
};
}
}
@@ -1,8 +1,5 @@
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.Extensions;
@@ -12,33 +9,8 @@ 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,
IApiDocumentUrlService apiDocumentUrlService,
IVariationContextAccessor variationContextAccessor)
: base(requestRoutingService, requestPreviewService, apiDocumentUrlService, variationContextAccessor)
public ChildrenSelector(IPublishedSnapshotAccessor publishedSnapshotAccessor, IRequestRoutingService requestRoutingService)
: base(publishedSnapshotAccessor, requestRoutingService)
{
}
@@ -1,8 +1,5 @@
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.Extensions;
@@ -12,33 +9,8 @@ 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,
IApiDocumentUrlService apiDocumentUrlService,
IVariationContextAccessor variationContextAccessor)
: base(requestRoutingService, requestPreviewService, apiDocumentUrlService, variationContextAccessor)
public DescendantsSelector(IPublishedSnapshotAccessor publishedSnapshotAccessor, IRequestRoutingService requestRoutingService)
: base(publishedSnapshotAccessor, requestRoutingService)
{
}
@@ -41,7 +41,7 @@ public class MemberApplicationManager : OpenIdDictApplicationManagerBase, IMembe
{
OpenIddictConstants.Permissions.Endpoints.Authorization,
OpenIddictConstants.Permissions.Endpoints.Token,
OpenIddictConstants.Permissions.Endpoints.EndSession,
OpenIddictConstants.Permissions.Endpoints.Logout,
OpenIddictConstants.Permissions.Endpoints.Revocation,
OpenIddictConstants.Permissions.GrantTypes.AuthorizationCode,
OpenIddictConstants.Permissions.GrantTypes.RefreshToken,
@@ -64,26 +64,4 @@ public class MemberApplicationManager : OpenIdDictApplicationManagerBase, IMembe
public async Task DeleteMemberApplicationAsync(CancellationToken cancellationToken = default)
=> await Delete(Constants.OAuthClientIds.Member, cancellationToken);
public async Task EnsureMemberClientCredentialsApplicationAsync(string clientId, string clientSecret, CancellationToken cancellationToken = default)
{
var applicationDescriptor = new OpenIddictApplicationDescriptor
{
DisplayName = $"Umbraco client credentials member access: {clientId}",
ClientId = clientId,
ClientSecret = clientSecret,
ClientType = OpenIddictConstants.ClientTypes.Confidential,
Permissions =
{
OpenIddictConstants.Permissions.Endpoints.Token,
OpenIddictConstants.Permissions.Endpoints.Revocation,
OpenIddictConstants.Permissions.GrantTypes.ClientCredentials
}
};
await CreateOrUpdate(applicationDescriptor, cancellationToken);
}
public async Task DeleteMemberClientCredentialsApplicationAsync(string clientId, CancellationToken cancellationToken = default)
=> await Delete(clientId, cancellationToken);
}
@@ -4,7 +4,6 @@ using Umbraco.Cms.Core.DeliveryApi;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.PublishedContent;
using Umbraco.Cms.Core.PublishedCache;
using Umbraco.Cms.Core.Services.Navigation;
using Umbraco.Cms.Core.Services.OperationStatus;
using Umbraco.Extensions;
@@ -13,15 +12,13 @@ namespace Umbraco.Cms.Api.Delivery.Services;
/// <inheritdoc />
internal sealed class ApiMediaQueryService : IApiMediaQueryService
{
private readonly IPublishedMediaCache _publishedMediaCache;
private readonly IPublishedSnapshotAccessor _publishedSnapshotAccessor;
private readonly ILogger<ApiMediaQueryService> _logger;
private readonly IMediaNavigationQueryService _mediaNavigationQueryService;
public ApiMediaQueryService(IPublishedMediaCache publishedMediaCache, ILogger<ApiMediaQueryService> logger, IMediaNavigationQueryService mediaNavigationQueryService)
public ApiMediaQueryService(IPublishedSnapshotAccessor publishedSnapshotAccessor, ILogger<ApiMediaQueryService> logger)
{
_publishedMediaCache = publishedMediaCache;
_publishedSnapshotAccessor = publishedSnapshotAccessor;
_logger = logger;
_mediaNavigationQueryService = mediaNavigationQueryService;
}
/// <inheritdoc/>
@@ -55,7 +52,8 @@ internal sealed class ApiMediaQueryService : IApiMediaQueryService
=> TryGetByPath(path, GetRequiredPublishedMediaCache());
private IPublishedMediaCache GetRequiredPublishedMediaCache()
=> _publishedMediaCache;
=> _publishedSnapshotAccessor.GetRequiredPublishedSnapshot().Media
?? throw new InvalidOperationException("Could not obtain the published media cache");
private IPublishedContent? TryGetByPath(string path, IPublishedMediaCache mediaCache)
{
@@ -71,7 +69,7 @@ internal sealed class ApiMediaQueryService : IApiMediaQueryService
break;
}
currentChildren = resolvedMedia.Children(null, _publishedMediaCache, _mediaNavigationQueryService);
currentChildren = resolvedMedia.Children;
}
return resolvedMedia;
@@ -104,7 +102,7 @@ internal sealed class ApiMediaQueryService : IApiMediaQueryService
? mediaCache.GetById(parentKey)
: TryGetByPath(childrenOf, mediaCache);
return parent?.Children(null, _publishedMediaCache, _mediaNavigationQueryService) ?? Array.Empty<IPublishedContent>();
return parent?.Children ?? Array.Empty<IPublishedContent>();
}
private IEnumerable<IPublishedContent>? ApplyFilters(IEnumerable<IPublishedContent> source, IEnumerable<string> filters)
@@ -21,7 +21,7 @@ internal sealed class RequestRedirectService : RoutingServiceBase, IRequestRedir
private readonly GlobalSettings _globalSettings;
public RequestRedirectService(
IDomainCache domainCache,
IPublishedSnapshotAccessor publishedSnapshotAccessor,
IHttpContextAccessor httpContextAccessor,
IRequestStartItemProviderAccessor requestStartItemProviderAccessor,
IRequestCultureService requestCultureService,
@@ -29,7 +29,7 @@ internal sealed class RequestRedirectService : RoutingServiceBase, IRequestRedir
IApiPublishedContentCache apiPublishedContentCache,
IApiContentRouteBuilder apiContentRouteBuilder,
IOptions<GlobalSettings> globalSettings)
: base(domainCache, httpContextAccessor, requestStartItemProviderAccessor)
: base(publishedSnapshotAccessor, httpContextAccessor, requestStartItemProviderAccessor)
{
_requestCultureService = requestCultureService;
_redirectUrlService = redirectUrlService;
@@ -42,31 +42,42 @@ internal sealed class RequestRedirectService : RoutingServiceBase, IRequestRedir
{
requestedPath = requestedPath.EnsureStartsWith("/");
IPublishedContent? startItem = GetStartItem();
// must append the root content url segment if it is not hidden by config, because
// the URL tracking is based on the actual URL, including the root content url segment
if (_globalSettings.HideTopLevelNodeFromPath == false)
if (_globalSettings.HideTopLevelNodeFromPath == false && startItem?.UrlSegment != null)
{
IPublishedContent? startItem = GetStartItem();
if (startItem?.UrlSegment != null)
{
requestedPath = $"{startItem.UrlSegment.EnsureStartsWith("/")}{requestedPath}";
}
requestedPath = $"{startItem.UrlSegment.EnsureStartsWith("/")}{requestedPath}";
}
var culture = _requestCultureService.GetRequestedCulture();
// append the configured domain content ID to the path if we have a domain bound request,
// because URL tracking registers the tracked url like "{domain content ID}/{content path}"
Uri contentRoute = GetDefaultRequestUri(requestedPath);
DomainAndUri? domainAndUri = GetDomainAndUriForRoute(contentRoute);
if (domainAndUri != null)
// important: redirect URLs are always tracked without trailing slashes
requestedPath = requestedPath.TrimEnd("/");
IRedirectUrl? redirectUrl = _redirectUrlService.GetMostRecentRedirectUrl(requestedPath, culture);
// if a redirect URL was not found, try by appending the start item ID because URL tracking might have tracked
// a redirect with "{root content ID}/{content path}"
if (redirectUrl is null && startItem is not null)
{
requestedPath = GetContentRoute(domainAndUri, contentRoute);
culture ??= domainAndUri.Culture;
redirectUrl = _redirectUrlService.GetMostRecentRedirectUrl($"{startItem.Id}{requestedPath}", culture);
}
// still no redirect URL found - try looking for a configured domain if we have a domain bound request,
// because URL tracking might have tracked a redirect with "{domain content ID}/{content path}"
if (redirectUrl is null)
{
Uri contentRoute = GetDefaultRequestUri(requestedPath);
DomainAndUri? domainAndUri = GetDomainAndUriForRoute(contentRoute);
if (domainAndUri is not null)
{
requestedPath = GetContentRoute(domainAndUri, contentRoute);
culture ??= domainAndUri.Culture;
redirectUrl = _redirectUrlService.GetMostRecentRedirectUrl(requestedPath, culture);
}
}
// important: redirect URLs are always tracked without trailing slashes
IRedirectUrl? redirectUrl = _redirectUrlService.GetMostRecentRedirectUrl(requestedPath.TrimEnd("/"), culture);
IPublishedContent? content = redirectUrl != null
? _apiPublishedContentCache.GetById(redirectUrl.ContentKey)
: null;
@@ -1,6 +1,5 @@
using Microsoft.AspNetCore.Http;
using Umbraco.Cms.Core.DeliveryApi;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.PublishedContent;
using Umbraco.Cms.Core.PublishedCache;
using Umbraco.Cms.Core.Routing;
@@ -13,11 +12,11 @@ internal sealed class RequestRoutingService : RoutingServiceBase, IRequestRoutin
private readonly IRequestCultureService _requestCultureService;
public RequestRoutingService(
IDomainCache domainCache,
IPublishedSnapshotAccessor publishedSnapshotAccessor,
IHttpContextAccessor httpContextAccessor,
IRequestStartItemProviderAccessor requestStartItemProviderAccessor,
IRequestCultureService requestCultureService)
: base(domainCache, httpContextAccessor, requestStartItemProviderAccessor) =>
: base(publishedSnapshotAccessor, httpContextAccessor, requestStartItemProviderAccessor) =>
_requestCultureService = requestCultureService;
/// <inheritdoc />
@@ -3,34 +3,29 @@ using Umbraco.Cms.Core;
using Umbraco.Cms.Core.DeliveryApi;
using Umbraco.Cms.Core.Models.PublishedContent;
using Umbraco.Cms.Core.PublishedCache;
using Umbraco.Cms.Core.Services.Navigation;
using Umbraco.Extensions;
namespace Umbraco.Cms.Api.Delivery.Services;
internal sealed class RequestStartItemProvider : RequestHeaderHandler, IRequestStartItemProvider
{
private readonly IPublishedSnapshotAccessor _publishedSnapshotAccessor;
private readonly IVariationContextAccessor _variationContextAccessor;
private readonly IRequestPreviewService _requestPreviewService;
private readonly IDocumentNavigationQueryService _documentNavigationQueryService;
private readonly IPublishedContentCache _publishedContentCache;
// this provider lifetime is Scope, so we can cache this as a field
private IPublishedContent? _requestedStartContent;
public RequestStartItemProvider(
IHttpContextAccessor httpContextAccessor,
IPublishedSnapshotAccessor publishedSnapshotAccessor,
IVariationContextAccessor variationContextAccessor,
IRequestPreviewService requestPreviewService,
IDocumentNavigationQueryService documentNavigationQueryService,
IPublishedContentCache publishedContentCache)
IRequestPreviewService requestPreviewService)
: base(httpContextAccessor)
{
_publishedSnapshotAccessor = publishedSnapshotAccessor;
_variationContextAccessor = variationContextAccessor;
_requestPreviewService = requestPreviewService;
_documentNavigationQueryService = documentNavigationQueryService;
_publishedContentCache = publishedContentCache;
}
/// <inheritdoc/>
@@ -47,10 +42,13 @@ internal sealed class RequestStartItemProvider : RequestHeaderHandler, IRequestS
return null;
}
_documentNavigationQueryService.TryGetRootKeys(out IEnumerable<Guid> rootKeys);
IEnumerable<IPublishedContent> rootContent = rootKeys
.Select(rootKey => _publishedContentCache.GetById(_requestPreviewService.IsPreview(), rootKey))
.WhereNotNull();
if (_publishedSnapshotAccessor.TryGetPublishedSnapshot(out IPublishedSnapshot? publishedSnapshot) == false ||
publishedSnapshot?.Content == null)
{
return null;
}
IEnumerable<IPublishedContent> rootContent = publishedSnapshot.Content.GetAtRoot(_requestPreviewService.IsPreview());
_requestedStartContent = Guid.TryParse(headerValue, out Guid key)
? rootContent.FirstOrDefault(c => c.Key == key)
@@ -9,16 +9,16 @@ namespace Umbraco.Cms.Api.Delivery.Services;
internal abstract class RoutingServiceBase
{
private readonly IDomainCache _domainCache;
private readonly IPublishedSnapshotAccessor _publishedSnapshotAccessor;
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly IRequestStartItemProviderAccessor _requestStartItemProviderAccessor;
protected RoutingServiceBase(
IDomainCache domainCache,
IPublishedSnapshotAccessor publishedSnapshotAccessor,
IHttpContextAccessor httpContextAccessor,
IRequestStartItemProviderAccessor requestStartItemProviderAccessor)
{
_domainCache = domainCache;
_publishedSnapshotAccessor = publishedSnapshotAccessor;
_httpContextAccessor = httpContextAccessor;
_requestStartItemProviderAccessor = requestStartItemProviderAccessor;
}
@@ -40,9 +40,15 @@ internal abstract class RoutingServiceBase
protected DomainAndUri? GetDomainAndUriForRoute(Uri contentUrl)
{
IEnumerable<Domain> domains = _domainCache.GetAll(false);
IDomainCache? domainCache = _publishedSnapshotAccessor.GetRequiredPublishedSnapshot().Domains;
if (domainCache == null)
{
throw new InvalidOperationException("Could not obtain the domain cache in the current context");
}
return DomainUtilities.SelectDomain(domains, contentUrl, defaultCulture: _domainCache.DefaultCulture);
IEnumerable<Domain> domains = domainCache.GetAll(false);
return DomainUtilities.SelectDomain(domains, contentUrl, defaultCulture: domainCache.DefaultCulture);
}
protected IPublishedContent? GetStartItem()
@@ -3,16 +3,11 @@
<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:
[ASP0019] use IHeaderDictionary.Append or the indexer to append or set headers
[CS0618/CS0612] update obsolete references
-->
<WarningsNotAsErrors>$(WarningsNotAsErrors),ASP0019,CS0618,CS0612</WarningsNotAsErrors>
<!-- TODO: [ASP0019] use IHeaderDictionary.Append or the indexer to append or set headers,
and remove this override -->
<WarningsNotAsErrors>ASP0019</WarningsNotAsErrors>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Umbraco.Cms.Api.Common\Umbraco.Cms.Api.Common.csproj" />
<ProjectReference Include="..\Umbraco.Web.Common\Umbraco.Web.Common.csproj" />
@@ -31,6 +31,7 @@ public class ConfigureUmbracoManagementApiSwaggerGenOptions : IConfigureOptions<
});
swaggerGenOptions.OperationFilter<ResponseHeaderOperationFilter>();
swaggerGenOptions.SelectSubTypesUsing(_umbracoJsonTypeInfoResolver.FindSubTypes);
swaggerGenOptions.UseOneOfForPolymorphism();
// Ensure all types that implements the IOpenApiDiscriminator have a $type property in the OpenApi schema with the default value (The class name) that is expected by the server
@@ -51,7 +51,7 @@ public class BackOfficeLoginController : Controller
if (string.IsNullOrEmpty(model.UmbracoUrl))
{
model.UmbracoUrl = _hostingEnvironment.ToAbsolute(Constants.System.DefaultUmbracoPath);
model.UmbracoUrl = _hostingEnvironment.ToAbsolute(_globalSettings.UmbracoPath);
}
if (string.IsNullOrEmpty(model.ReturnUrl))
@@ -14,7 +14,7 @@ namespace Umbraco.Cms.Api.Management.Controllers.Content;
public abstract class ContentCollectionControllerBase<TContent, TCollectionResponseModel, TValueResponseModelBase, TVariantResponseModel> : ManagementApiControllerBase
where TContent : class, IContentBase
where TCollectionResponseModel : ContentResponseModelBase<TValueResponseModelBase, TVariantResponseModel>
where TValueResponseModelBase : ValueResponseModelBase
where TValueResponseModelBase : ValueModelBase
where TVariantResponseModel : VariantResponseModelBase
{
private readonly IUmbracoMapper _mapper;
@@ -4,7 +4,6 @@ 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;
using Umbraco.Cms.Core.Services.OperationStatus;
using Umbraco.Extensions;
@@ -101,7 +100,7 @@ public abstract class ContentControllerBase : ManagementApiControllerBase
var errors = new SortedDictionary<string, string[]>();
var validationErrorExpressionRoot = $"$.{nameof(ContentModelBase<TValueModel, TVariantModel>.Values).ToFirstLowerInvariant()}";
var missingPropertyModels = new List<PropertyValidationResponseModel>();
foreach (PropertyValidationError validationError in validationResult.ValidationErrors)
{
TValueModel? requestValue = requestModel.Values.FirstOrDefault(value =>
@@ -110,16 +109,13 @@ public abstract class ContentControllerBase : ManagementApiControllerBase
&& value.Segment == validationError.Segment);
if (requestValue is null)
{
errors.Add(
$"{validationErrorExpressionRoot}[{JsonPathExpression.MissingPropertyValue(validationError.Alias, validationError.Culture, validationError.Segment)}].{nameof(ValueModelBase.Value)}",
validationError.ErrorMessages);
missingPropertyModels.Add(MapMissingProperty(validationError));
continue;
}
var index = requestModel.Values.IndexOf(requestValue);
errors.Add(
$"$.{nameof(ContentModelBase<TValueModel, TVariantModel>.Values).ToFirstLowerInvariant()}[{index}].{nameof(ValueModelBase.Value).ToFirstLowerInvariant()}{validationError.JsonPath}",
validationError.ErrorMessages);
var key = $"$.{nameof(ContentModelBase<TValueModel, TVariantModel>.Values).ToFirstLowerInvariant()}[{index}].{nameof(ValueModelBase.Value).ToFirstLowerInvariant()}{validationError.JsonPath}";
errors.Add(key, validationError.ErrorMessages);
}
return OperationStatusResult(status, problemDetailsBuilder
@@ -127,6 +123,16 @@ public abstract class ContentControllerBase : ManagementApiControllerBase
.WithTitle("Validation failed")
.WithDetail("One or more properties did not pass validation")
.WithRequestModelErrors(errors)
.WithExtension("missingValues", missingPropertyModels.ToArray())
.Build()));
}
private PropertyValidationResponseModel MapMissingProperty(PropertyValidationError source) =>
new()
{
Alias = source.Alias,
Segment = source.Segment,
Culture = source.Culture,
Messages = source.ErrorMessages,
};
}
@@ -11,7 +11,7 @@ namespace Umbraco.Cms.Api.Management.Controllers.DataType;
[VersionedApiBackOfficeRoute(Constants.UdiEntityType.DataType)]
[ApiExplorerSettings(GroupName = "Data Type")]
[Authorize(Policy = AuthorizationPolicies.TreeAccessDocumentsOrMediaOrMembersOrContentTypes)]
[Authorize(Policy = AuthorizationPolicies.TreeAccessDocumentOrMediaOrContentTypes)]
public abstract class DataTypeControllerBase : ManagementApiControllerBase
{
protected IActionResult DataTypeOperationStatusResult(DataTypeOperationStatus status) =>
@@ -2,14 +2,13 @@
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.Security.Authorization.Content;
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.Authorization;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Services.Querying;
using Umbraco.Cms.Web.Common.Authorization;
using Umbraco.Extensions;
@@ -19,42 +18,17 @@ namespace Umbraco.Cms.Api.Management.Controllers.Document;
public class ByKeyDocumentController : DocumentControllerBase
{
private readonly IAuthorizationService _authorizationService;
private readonly IContentEditingService _contentEditingService;
private readonly IDocumentPresentationFactory _documentPresentationFactory;
private readonly IContentQueryService _contentQueryService;
[Obsolete("Scheduled for removal in v17")]
public ByKeyDocumentController(
IAuthorizationService authorizationService,
IContentEditingService contentEditingService,
IDocumentPresentationFactory documentPresentationFactory)
{
_authorizationService = authorizationService;
_contentEditingService = contentEditingService;
_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,
IContentQueryService contentQueryService)
{
_authorizationService = authorizationService;
_documentPresentationFactory = documentPresentationFactory;
_contentQueryService = contentQueryService;
}
[HttpGet("{id:guid}")]
@@ -73,16 +47,13 @@ public class ByKeyDocumentController : DocumentControllerBase
return Forbidden();
}
var contentWithScheduleAttempt = await _contentQueryService.GetWithSchedulesAsync(id);
if (contentWithScheduleAttempt.Success == false)
IContent? content = await _contentEditingService.GetAsync(id);
if (content == null)
{
return ContentQueryOperationStatusResult(contentWithScheduleAttempt.Status);
return DocumentNotFound();
}
DocumentResponseModel model = await _documentPresentationFactory.CreateResponseModelAsync(
contentWithScheduleAttempt.Result!.Content,
contentWithScheduleAttempt.Result.Schedules);
DocumentResponseModel model = await _documentPresentationFactory.CreateResponseModelAsync(content);
return Ok(model);
}
}
@@ -1,58 +0,0 @@
using Asp.Versioning;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Api.Management.Factories;
using Umbraco.Cms.Api.Management.ViewModels.Document;
using Umbraco.Cms.Core.Actions;
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 ByKeyPublishedDocumentController : DocumentControllerBase
{
private readonly IAuthorizationService _authorizationService;
private readonly IContentEditingService _contentEditingService;
private readonly IDocumentPresentationFactory _documentPresentationFactory;
public ByKeyPublishedDocumentController(
IAuthorizationService authorizationService,
IContentEditingService contentEditingService,
IDocumentPresentationFactory documentPresentationFactory)
{
_authorizationService = authorizationService;
_contentEditingService = contentEditingService;
_documentPresentationFactory = documentPresentationFactory;
}
[HttpGet("{id:guid}/published")]
[MapToApiVersion("1.0")]
[ProducesResponseType(typeof(PublishedDocumentResponseModel), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
public async Task<IActionResult> ByKeyPublished(CancellationToken cancellationToken, Guid id)
{
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 || content.Published is false)
{
return DocumentNotFound();
}
PublishedDocumentResponseModel model = await _documentPresentationFactory.CreatePublishedResponseModelAsync(content);
return Ok(model);
}
}
@@ -15,7 +15,7 @@ namespace Umbraco.Cms.Api.Management.Controllers.Document.Collection;
[VersionedApiBackOfficeRoute($"{Constants.Web.RoutePath.Collection}/{Constants.UdiEntityType.Document}")]
[ApiExplorerSettings(GroupName = nameof(Constants.UdiEntityType.Document))]
[Authorize(Policy = AuthorizationPolicies.TreeAccessDocuments)]
public abstract class DocumentCollectionControllerBase : ContentCollectionControllerBase<IContent, DocumentCollectionResponseModel, DocumentValueResponseModel, DocumentVariantResponseModel>
public abstract class DocumentCollectionControllerBase : ContentCollectionControllerBase<IContent, DocumentCollectionResponseModel, DocumentValueModel, DocumentVariantResponseModel>
{
protected DocumentCollectionControllerBase(IUmbracoMapper mapper)
: base(mapper)
@@ -170,15 +170,4 @@ public abstract class DocumentControllerBase : ContentControllerBase
.WithTitle("Unknown content operation status.")
.Build()),
});
protected IActionResult ContentQueryOperationStatusResult(ContentQueryOperationStatus status)
=> OperationStatusResult(status, problemDetailsBuilder => status switch
{
ContentQueryOperationStatus.ContentNotFound => NotFound(problemDetailsBuilder
.WithTitle("The document could not be found")
.Build()),
_ => StatusCode(StatusCodes.Status500InternalServerError, problemDetailsBuilder
.WithTitle("Unknown content query status.")
.Build()),
});
}
@@ -49,7 +49,7 @@ public class GetPublicAccessDocumentController : DocumentControllerBase
}
Attempt<PublicAccessEntry?, PublicAccessOperationStatus> accessAttempt =
await _publicAccessService.GetEntryByContentKeyWithoutAncestorsAsync(id);
await _publicAccessService.GetEntryByContentKeyAsync(id);
if (accessAttempt.Success is false || accessAttempt.Result is null)
{
@@ -46,7 +46,7 @@ public class PublishDocumentController : DocumentControllerBase
{
AuthorizationResult authorizationResult = await _authorizationService.AuthorizeResourceAsync(
User,
ContentPermissionResource.WithKeys(ActionPublish.ActionLetter, id, requestModel.PublishSchedules.Where(x => x.Culture is not null).Select(x=>x.Culture!)),
ContentPermissionResource.WithKeys(ActionPublish.ActionLetter, id, requestModel.PublishSchedules.Where(x=>x.Culture is not null).Select(x=>x.Culture!)),
AuthorizationPolicies.ContentPermissionByResource);
if (!authorizationResult.Succeeded)
@@ -54,7 +54,7 @@ public class PublishDocumentController : DocumentControllerBase
return Forbidden();
}
Attempt<List<CulturePublishScheduleModel>, ContentPublishingOperationStatus> modelResult = _documentPresentationFactory.CreateCulturePublishScheduleModels(requestModel);
Attempt<CultureAndScheduleModel, ContentPublishingOperationStatus> modelResult = _documentPresentationFactory.CreateCultureAndScheduleModel(requestModel);
if (modelResult.Success is false)
{
@@ -3,7 +3,6 @@ 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;
@@ -23,7 +22,7 @@ public class AreReferencedDocumentController : DocumentControllerBase
}
/// <summary>
/// Gets a paged list of the items used in any kind of relation from selected keys.
/// Gets a page list of the items used in any kind of relation from selected keys.
/// </summary>
/// <remarks>
/// Used when bulk deleting content/media and bulk unpublishing content (delete and unpublish on List view).
@@ -38,11 +37,11 @@ public class AreReferencedDocumentController : DocumentControllerBase
int skip = 0,
int take = 20)
{
PagedModel<Guid> distinctByKeyItemsWithReferencedRelations = await _trackedReferencesSkipTakeService.GetPagedKeysWithDependentReferencesAsync(ids, Constants.ObjectTypes.Document, skip, take);
PagedModel<RelationItemModel> distinctByKeyItemsWithReferencedRelations = await _trackedReferencesSkipTakeService.GetPagedItemsWithRelationsAsync(ids, skip, take, true);
var pagedViewModel = new PagedViewModel<ReferenceByIdModel>
{
Total = distinctByKeyItemsWithReferencedRelations.Total,
Items = _umbracoMapper.MapEnumerable<Guid, ReferenceByIdModel>(distinctByKeyItemsWithReferencedRelations.Items),
Items = _umbracoMapper.MapEnumerable<RelationItemModel, ReferenceByIdModel>(distinctByKeyItemsWithReferencedRelations.Items),
};
return await Task.FromResult(pagedViewModel);
@@ -22,7 +22,7 @@ public class ReferencedByDocumentController : DocumentControllerBase
}
/// <summary>
/// Gets a paged list of tracked references for the current item, so you can see where an item is being used.
/// 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.
@@ -22,7 +22,7 @@ public class ReferencedDescendantsDocumentController : DocumentControllerBase
}
/// <summary>
/// Gets a paged list of the descendant nodes of the current item used in any kind of relation.
/// 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
@@ -1,4 +1,4 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Api.Management.Controllers.Tree;
using Umbraco.Cms.Api.Management.Factories;
@@ -54,7 +54,6 @@ public abstract class DocumentTreeControllerBase : UserStartNodeTreeControllerBa
responseModel.IsProtected = _publicAccessService.IsProtected(entity.Path);
responseModel.IsTrashed = entity.Trashed;
responseModel.Id = entity.Key;
responseModel.CreateDate = entity.CreateDate;
responseModel.Variants = _documentPresentationFactory.CreateVariantsItemResponseModels(documentEntitySlim);
responseModel.DocumentType = _documentPresentationFactory.CreateDocumentTypeReferenceResponseModel(documentEntitySlim);
@@ -12,7 +12,6 @@ using Umbraco.Cms.Core.Services.OperationStatus;
namespace Umbraco.Cms.Api.Management.Controllers.Document;
[ApiVersion("1.0")]
[ApiVersion("1.1")]
public class ValidateUpdateDocumentController : UpdateDocumentControllerBase
{
private readonly IContentEditingService _contentEditingService;
@@ -33,35 +32,10 @@ public class ValidateUpdateDocumentController : UpdateDocumentControllerBase
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
[Obsolete("Please use version 1.1 of this API. Will be removed in V16.")]
public async Task<IActionResult> Validate(CancellationToken cancellationToken, Guid id, UpdateDocumentRequestModel requestModel)
=> await HandleRequest(id, requestModel, async () =>
{
var validateUpdateDocumentRequestModel = new ValidateUpdateDocumentRequestModel
{
Values = requestModel.Values,
Variants = requestModel.Variants,
Template = requestModel.Template,
Cultures = null
};
ValidateContentUpdateModel model = _documentEditingPresentationFactory.MapValidateUpdateModel(validateUpdateDocumentRequestModel);
Attempt<ContentValidationResult, ContentEditingOperationStatus> result = await _contentEditingService.ValidateUpdateAsync(id, model);
return result.Success
? Ok()
: DocumentEditingOperationStatusResult(result.Status, requestModel, result.Result);
});
[HttpPut("{id:guid}/validate")]
[MapToApiVersion("1.1")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
public async Task<IActionResult> ValidateV1_1(CancellationToken cancellationToken, Guid id, ValidateUpdateDocumentRequestModel requestModel)
=> await HandleRequest(id, requestModel, async () =>
{
ValidateContentUpdateModel model = _documentEditingPresentationFactory.MapValidateUpdateModel(requestModel);
ContentUpdateModel model = _documentEditingPresentationFactory.MapUpdateModel(requestModel);
Attempt<ContentValidationResult, ContentEditingOperationStatus> result = await _contentEditingService.ValidateUpdateAsync(id, model);
return result.Success
@@ -32,7 +32,7 @@ public class ItemDocumentTypeItemController : DocumentTypeItemControllerBase
return Ok(Enumerable.Empty<DocumentTypeItemResponseModel>());
}
IEnumerable<IContentType> contentTypes = _contentTypeService.GetMany(ids);
IEnumerable<IContentType> contentTypes = _contentTypeService.GetAll(ids);
List<DocumentTypeItemResponseModel> responseModels = _mapper.MapEnumerable<IContentType, DocumentTypeItemResponseModel>(contentTypes);
return await Task.FromResult(Ok(responseModels));
}
@@ -35,7 +35,7 @@ public class SearchDocumentTypeItemController : DocumentTypeItemControllerBase
return await Task.FromResult(Ok(new PagedModel<DocumentTypeItemResponseModel> { Total = searchResult.Total }));
}
IEnumerable<IContentType> contentTypes = _contentTypeService.GetMany(searchResult.Items.Select(item => item.Key).ToArray().EmptyNull());
IEnumerable<IContentType> contentTypes = _contentTypeService.GetAll(searchResult.Items.Select(item => item.Key).ToArray().EmptyNull());
var result = new PagedModel<DocumentTypeItemResponseModel>
{
Items = _mapper.MapEnumerable<IContentType, DocumentTypeItemResponseModel>(contentTypes),
@@ -29,7 +29,7 @@ public class DocumentTypeTreeControllerBase : FolderTreeControllerBase<DocumentT
protected override DocumentTypeTreeItemResponseModel[] MapTreeItemViewModels(Guid? parentKey, IEntitySlim[] entities)
{
var contentTypes = _contentTypeService
.GetMany(entities.Select(entity => entity.Id).ToArray())
.GetAll(entities.Select(entity => entity.Id).ToArray())
.ToDictionary(contentType => contentType.Id);
return entities.Select(entity =>
@@ -15,7 +15,7 @@ namespace Umbraco.Cms.Api.Management.Controllers.Media.Collection;
[VersionedApiBackOfficeRoute($"{Constants.Web.RoutePath.Collection}/{Constants.UdiEntityType.Media}")]
[ApiExplorerSettings(GroupName = nameof(Constants.UdiEntityType.Media))]
[Authorize(Policy = AuthorizationPolicies.SectionAccessMedia)]
public abstract class MediaCollectionControllerBase : ContentCollectionControllerBase<IMedia, MediaCollectionResponseModel, MediaValueResponseModel, MediaVariantResponseModel>
public abstract class MediaCollectionControllerBase : ContentCollectionControllerBase<IMedia, MediaCollectionResponseModel, MediaValueModel, MediaVariantResponseModel>
{
protected MediaCollectionControllerBase(IUmbracoMapper mapper)
: base(mapper)
@@ -3,7 +3,6 @@ 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;
@@ -39,11 +38,11 @@ public class AreReferencedMediaController : MediaControllerBase
int skip = 0,
int take = 20)
{
PagedModel<Guid> distinctByKeyItemsWithReferencedRelations = await _trackedReferencesSkipTakeService.GetPagedKeysWithDependentReferencesAsync(ids, Constants.ObjectTypes.Media, skip, take);
PagedModel<RelationItemModel> distinctByKeyItemsWithReferencedRelations = await _trackedReferencesSkipTakeService.GetPagedItemsWithRelationsAsync(ids, skip, take, true);
var pagedViewModel = new PagedViewModel<ReferenceByIdModel>
{
Total = distinctByKeyItemsWithReferencedRelations.Total,
Items = _umbracoMapper.MapEnumerable<Guid, ReferenceByIdModel>(distinctByKeyItemsWithReferencedRelations.Items),
Items = _umbracoMapper.MapEnumerable<RelationItemModel, ReferenceByIdModel>(distinctByKeyItemsWithReferencedRelations.Items),
};
return await Task.FromResult(pagedViewModel);
@@ -1,4 +1,4 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Api.Management.Controllers.Tree;
using Umbraco.Cms.Api.Management.Factories;
@@ -50,7 +50,6 @@ public class MediaTreeControllerBase : UserStartNodeTreeControllerBase<MediaTree
{
responseModel.IsTrashed = entity.Trashed;
responseModel.Id = entity.Key;
responseModel.CreateDate = entity.CreateDate;
responseModel.Variants = _mediaPresentationFactory.CreateVariantsItemResponseModels(mediaEntitySlim);
responseModel.MediaType = _mediaPresentationFactory.CreateMediaTypeReferenceResponseModel(mediaEntitySlim);
@@ -32,7 +32,7 @@ public class ItemMediaTypeItemController : MediaTypeItemControllerBase
return Ok(Enumerable.Empty<MediaTypeItemResponseModel>());
}
IEnumerable<IMediaType> mediaTypes = _mediaTypeService.GetMany(ids);
IEnumerable<IMediaType> mediaTypes = _mediaTypeService.GetAll(ids);
List<MediaTypeItemResponseModel> responseModels = _mapper.MapEnumerable<IMediaType, MediaTypeItemResponseModel>(mediaTypes);
return await Task.FromResult(Ok(responseModels));
}
@@ -35,7 +35,7 @@ public class SearchMediaTypeItemController : MediaTypeItemControllerBase
return await Task.FromResult(Ok(new PagedModel<MediaTypeItemResponseModel> { Total = searchResult.Total }));
}
IEnumerable<IMediaType> mediaTypes = _mediaTypeService.GetMany(searchResult.Items.Select(item => item.Key).ToArray().EmptyNull());
IEnumerable<IMediaType> mediaTypes = _mediaTypeService.GetAll(searchResult.Items.Select(item => item.Key).ToArray().EmptyNull());
var result = new PagedModel<MediaTypeItemResponseModel>
{
Items = _mapper.MapEnumerable<IMediaType, MediaTypeItemResponseModel>(mediaTypes),
@@ -30,7 +30,7 @@ public class MediaTypeTreeControllerBase : FolderTreeControllerBase<MediaTypeTre
protected override MediaTypeTreeItemResponseModel[] MapTreeItemViewModels(Guid? parentKey, IEntitySlim[] entities)
{
var mediaTypes = _mediaTypeService
.GetMany(entities.Select(entity => entity.Id).ToArray())
.GetAll(entities.Select(entity => entity.Id).ToArray())
.ToDictionary(contentType => contentType.Id);
return entities.Select(entity =>
@@ -32,7 +32,7 @@ public class ItemMemberTypeItemController : MemberTypeItemControllerBase
return Ok(Enumerable.Empty<MemberTypeItemResponseModel>());
}
IEnumerable<IMemberType> memberTypes = _memberTypeService.GetMany(ids);
IEnumerable<IMemberType> memberTypes = _memberTypeService.GetAll(ids);
List<MemberTypeItemResponseModel> responseModels = _mapper.MapEnumerable<IMemberType, MemberTypeItemResponseModel>(memberTypes);
return Ok(responseModels);
}
@@ -34,7 +34,7 @@ public class SearchMemberTypeItemController : MemberTypeItemControllerBase
return await Task.FromResult(Ok(new PagedModel<MemberTypeItemResponseModel> { Total = searchResult.Total }));
}
IEnumerable<IMemberType> memberTypes = _memberTypeService.GetMany(searchResult.Items.Select(item => item.Key).ToArray());
IEnumerable<IMemberType> memberTypes = _memberTypeService.GetAll(searchResult.Items.Select(item => item.Key).ToArray());
var result = new PagedModel<MemberTypeItemResponseModel>
{
Items = _mapper.MapEnumerable<IMemberType, MemberTypeItemResponseModel>(memberTypes),
@@ -27,7 +27,7 @@ public class MemberTypeTreeControllerBase : NamedEntityTreeControllerBase<Member
protected override MemberTypeTreeItemResponseModel[] MapTreeItemViewModels(Guid? parentKey, IEntitySlim[] entities)
{
var memberTypes = _memberTypeService
.GetMany(entities.Select(entity => entity.Id).ToArray())
.GetAll(entities.Select(entity => entity.Id).ToArray())
.ToDictionary(contentType => contentType.Id);
return entities.Select(entity =>
@@ -1,18 +1,25 @@
using Asp.Versioning;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Core.PublishedCache;
namespace Umbraco.Cms.Api.Management.Controllers.PublishedCache;
[Obsolete("This controller no longer serves a purpose")]
[ApiVersion("1.0")]
public class CollectPublishedCacheController : PublishedCacheControllerBase
{
private readonly IPublishedSnapshotService _publishedSnapshotService;
public CollectPublishedCacheController(IPublishedSnapshotService publishedSnapshotService)
=> _publishedSnapshotService = publishedSnapshotService;
[HttpPost("collect")]
[MapToApiVersion("1.0")]
[ProducesResponseType(StatusCodes.Status501NotImplemented)]
[ProducesResponseType(StatusCodes.Status200OK)]
public async Task<IActionResult> Collect(CancellationToken cancellationToken)
{
return StatusCode(StatusCodes.Status501NotImplemented);
GC.Collect();
await _publishedSnapshotService.CollectAsync();
return Ok();
}
}
@@ -8,16 +8,17 @@ namespace Umbraco.Cms.Api.Management.Controllers.PublishedCache;
[ApiVersion("1.0")]
public class RebuildPublishedCacheController : PublishedCacheControllerBase
{
private readonly IDatabaseCacheRebuilder _databaseCacheRebuilder;
private readonly IPublishedSnapshotService _publishedSnapshotService;
public RebuildPublishedCacheController(IDatabaseCacheRebuilder databaseCacheRebuilder) => _databaseCacheRebuilder = databaseCacheRebuilder;
public RebuildPublishedCacheController(IPublishedSnapshotService publishedSnapshotService)
=> _publishedSnapshotService = publishedSnapshotService;
[HttpPost("rebuild")]
[MapToApiVersion("1.0")]
[ProducesResponseType(StatusCodes.Status200OK)]
public async Task<IActionResult> Rebuild(CancellationToken cancellationToken)
{
_databaseCacheRebuilder.Rebuild();
_publishedSnapshotService.Rebuild();
return await Task.FromResult(Ok());
}
}
@@ -1,18 +1,21 @@
using Asp.Versioning;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Core.PublishedCache;
namespace Umbraco.Cms.Api.Management.Controllers.PublishedCache;
[ApiVersion("1.0")]
[Obsolete("This no longer relevant since snapshots are no longer used")]
public class StatusPublishedCacheController : PublishedCacheControllerBase
{
private readonly IPublishedSnapshotStatus _publishedSnapshotStatus;
public StatusPublishedCacheController(IPublishedSnapshotStatus publishedSnapshotStatus)
=> _publishedSnapshotStatus = publishedSnapshotStatus;
[HttpGet("status")]
[MapToApiVersion("1.0")]
[ProducesResponseType(StatusCodes.Status501NotImplemented)]
[ProducesResponseType(typeof(string), StatusCodes.Status200OK)]
public async Task<ActionResult<string>> Status(CancellationToken cancellationToken)
{
return StatusCode(StatusCodes.Status501NotImplemented);
}
=> await Task.FromResult(Ok(_publishedSnapshotStatus.GetStatus()));
}
@@ -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.Controllers.Content;
@@ -54,7 +54,6 @@ public abstract class RecycleBinControllerBase<TItem> : ContentControllerBase
var viewModel = new TItem
{
Id = entity.Key,
CreateDate = entity.CreateDate,
HasChildren = entity.HasChildren,
Parent = parentKey.HasValue
? new ItemReferenceByIdResponseModel
@@ -1,6 +1,5 @@
using Asp.Versioning;
using Examine;
using Examine.Lucene.Search;
using Examine.Search;
using Lucene.Net.QueryParsers.Classic;
using Microsoft.AspNetCore.Http;
@@ -53,13 +52,12 @@ public class QuerySearcherController : SearcherControllerBase
ISearchResults results;
// NativeQuery will work for a single word/phrase too (but depends on the implementation) the lucene one will work.
// Due to examine changes we need to supply the skipTakeMaxResults, see https://github.com/umbraco/Umbraco-CMS/issues/17920 for more info
try
{
results = searcher
.CreateQuery()
.NativeQuery(term)
.Execute(new LuceneQueryOptions(skip, take, skipTakeMaxResults: skip + take));
.Execute(QueryOptions.SkipTake(skip, take));
}
catch (ParseException)
{
@@ -44,7 +44,6 @@ public class BackOfficeController : SecurityControllerBase
private readonly IBackOfficeTwoFactorOptions _backOfficeTwoFactorOptions;
private readonly IUserTwoFactorLoginService _userTwoFactorLoginService;
private readonly IBackOfficeExternalLoginService _externalLoginService;
private readonly IBackOfficeUserClientCredentialsManager _backOfficeUserClientCredentialsManager;
private const string RedirectFlowParameter = "flow";
private const string RedirectStatusParameter = "status";
@@ -58,8 +57,7 @@ public class BackOfficeController : SecurityControllerBase
ILogger<BackOfficeController> logger,
IBackOfficeTwoFactorOptions backOfficeTwoFactorOptions,
IUserTwoFactorLoginService userTwoFactorLoginService,
IBackOfficeExternalLoginService externalLoginService,
IBackOfficeUserClientCredentialsManager backOfficeUserClientCredentialsManager)
IBackOfficeExternalLoginService externalLoginService)
{
_httpContextAccessor = httpContextAccessor;
_backOfficeSignInManager = backOfficeSignInManager;
@@ -69,7 +67,6 @@ public class BackOfficeController : SecurityControllerBase
_backOfficeTwoFactorOptions = backOfficeTwoFactorOptions;
_userTwoFactorLoginService = userTwoFactorLoginService;
_externalLoginService = externalLoginService;
_backOfficeUserClientCredentialsManager = backOfficeUserClientCredentialsManager;
}
[HttpPost("login")]
@@ -191,21 +188,13 @@ public class BackOfficeController : SecurityControllerBase
OpenIddictRequest? request = context.GetOpenIddictServerRequest();
if (request == null)
{
return BadRequest(new OpenIddictResponse
{
Error = "No context found",
ErrorDescription = "Unable to obtain context from the current request."
});
return BadRequest("Unable to obtain OpenID data from the current request");
}
// make sure we keep member authentication away from this endpoint
if (request.ClientId is Constants.OAuthClientIds.Member)
{
return BadRequest(new OpenIddictResponse
{
Error = "Invalid 'client ID'",
ErrorDescription = "The specified 'client_id' is not valid."
});
return BadRequest("The specified client ID cannot be used here.");
}
return request.IdentityProvider.IsNullOrWhiteSpace()
@@ -213,63 +202,6 @@ public class BackOfficeController : SecurityControllerBase
: await AuthorizeExternal(request);
}
[AllowAnonymous]
[HttpPost("token")]
[MapToApiVersion("1.0")]
public async Task<IActionResult> Token()
{
HttpContext context = _httpContextAccessor.GetRequiredHttpContext();
OpenIddictRequest? request = context.GetOpenIddictServerRequest();
if (request == null)
{
return BadRequest(new OpenIddictResponse
{
Error = "No context found",
ErrorDescription = "Unable to obtain context from the current request."
});
}
if (request.IsAuthorizationCodeGrantType() || request.IsRefreshTokenGrantType())
{
// attempt to authorize against the supplied the authorization code
AuthenticateResult authenticateResult = await context.AuthenticateAsync(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
return authenticateResult is { Succeeded: true, Principal: not null }
? new SignInResult(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme, authenticateResult.Principal)
: BadRequest(new OpenIddictResponse
{
Error = "Authorization failed",
ErrorDescription = "The supplied authorization could not be verified."
});
}
// ensure the client ID and secret are valid (verified by OpenIddict)
if (!request.IsClientCredentialsGrantType())
{
throw new InvalidOperationException("The requested grant type is not supported.");
}
// grab the user associated with the client ID
BackOfficeIdentityUser? associatedUser = await _backOfficeUserClientCredentialsManager.FindUserAsync(request.ClientId!);
if (associatedUser is not null)
{
// log current datetime as last login (this also ensures that the user is not flagged as inactive)
associatedUser.LastLoginDateUtc = DateTime.UtcNow;
await _backOfficeUserManager.UpdateAsync(associatedUser);
return await SignInBackOfficeUser(associatedUser, request);
}
// if this happens, the OpenIddict applications have somehow gone out of sync with the Umbraco users
_logger.LogError("The user associated with the client ID ({clientId}) could not be found", request.ClientId);
return BadRequest(new OpenIddictResponse
{
Error = "Authorization failed",
ErrorDescription = "The user associated with the supplied 'client_id' could not be found."
});
}
[AllowAnonymous]
[HttpGet("signout")]
[MapToApiVersion("1.0")]
@@ -518,7 +450,7 @@ public class BackOfficeController : SecurityControllerBase
private static IActionResult DefaultChallengeResult() => new ChallengeResult(Constants.Security.BackOfficeAuthenticationType);
private RedirectResult CallbackErrorRedirectWithStatus(string flowType, string status, IEnumerable<IdentityError> identityErrors)
private RedirectResult CallbackErrorRedirectWithStatus( string flowType, string status, IEnumerable<IdentityError> identityErrors)
{
var redirectUrl = _securitySettings.Value.BackOfficeHost + "/" +
_securitySettings.Value.AuthorizeCallbackErrorPathName.TrimStart('/').AppendQueryStringToUrl(
@@ -1,75 +0,0 @@
using Asp.Versioning;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.StaticFiles;
using Microsoft.Extensions.FileProviders;
using Microsoft.Extensions.Options;
using Umbraco.Cms.Api.Management.Routing;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Configuration.Models;
namespace Umbraco.Cms.Api.Management.Controllers.Security;
[ApiVersion("1.0")]
[VersionedApiBackOfficeRoute(Common.Security.Paths.BackOfficeApi.EndpointTemplate + "/graphics")]
[ApiExplorerSettings(IgnoreApi = true)]
public class BackOfficeGraphicsController : Controller
{
public const string LogoRouteName = nameof(BackOfficeGraphicsController) + "." + nameof(Logo);
public const string LoginBackGroundRouteName = nameof(BackOfficeGraphicsController) + "." + nameof(LoginBackground);
public const string LoginLogoRouteName = nameof(BackOfficeGraphicsController) + "." + nameof(LoginLogo);
public const string LoginLogoAlternativeRouteName = nameof(BackOfficeGraphicsController) + "." + nameof(LoginLogoAlternative);
private readonly IOptions<ContentSettings> _contentSettings;
private readonly IContentTypeProvider _contentTypeProvider;
private readonly IWebHostEnvironment _webHostEnvironment;
public BackOfficeGraphicsController(IOptions<ContentSettings> contentSettings, IOptions<StaticFileOptions> staticFileOptions, IWebHostEnvironment webHostEnvironment)
{
_contentSettings = contentSettings;
_webHostEnvironment = webHostEnvironment;
_contentTypeProvider = staticFileOptions.Value.ContentTypeProvider ?? new FileExtensionContentTypeProvider();
}
[HttpGet("login-background", Name = LoginBackGroundRouteName)]
[AllowAnonymous]
[MapToApiVersion("1.0")]
public IActionResult LoginBackground() => HandleFileRequest(_contentSettings.Value.LoginBackgroundImage);
[HttpGet("logo", Name = LogoRouteName)]
[AllowAnonymous]
[MapToApiVersion("1.0")]
public IActionResult Logo() => HandleFileRequest(_contentSettings.Value.BackOfficeLogo);
[HttpGet("login-logo", Name = LoginLogoRouteName)]
[AllowAnonymous]
[MapToApiVersion("1.0")]
public IActionResult LoginLogo() => HandleFileRequest(_contentSettings.Value.LoginLogoImage);
[HttpGet("login-logo-alternative", Name = LoginLogoAlternativeRouteName)]
[AllowAnonymous]
[MapToApiVersion("1.0")]
public IActionResult LoginLogoAlternative() => HandleFileRequest(_contentSettings.Value.LoginLogoImageAlternative);
private IActionResult HandleFileRequest(string virtualPath)
{
var filePath = $"{Constants.SystemDirectories.Umbraco}/{virtualPath}".TrimStart(Constants.CharArrays.Tilde);
IFileInfo fileInfo = _webHostEnvironment.WebRootFileProvider.GetFileInfo(filePath);
if (fileInfo.PhysicalPath is null)
{
return NotFound();
}
if (_contentTypeProvider.TryGetContentType(fileInfo.PhysicalPath, out var contentType))
{
Stream fileStream = fileInfo.CreateReadStream();
return File(fileStream, contentType);
}
return StatusCode(StatusCodes.Status412PreconditionFailed);
}
}
@@ -1,10 +1,8 @@
using Asp.Versioning;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using Umbraco.Cms.Api.Management.Security;
using Umbraco.Cms.Api.Management.ViewModels.Server;
using Umbraco.Cms.Core.Configuration.Models;
using Umbraco.Cms.Core.DependencyInjection;
@@ -16,7 +14,6 @@ public class ConfigurationServerController : ServerControllerBase
{
private readonly SecuritySettings _securitySettings;
private readonly GlobalSettings _globalSettings;
private readonly IBackOfficeExternalLoginProviders _externalLoginProviders;
[Obsolete("Use the constructor that accepts all arguments. Will be removed in V16.")]
public ConfigurationServerController(IOptions<SecuritySettings> securitySettings)
@@ -24,21 +21,13 @@ public class ConfigurationServerController : ServerControllerBase
{
}
[Obsolete("Use the constructor that accepts all arguments. Will be removed in V16.")]
public ConfigurationServerController(IOptions<SecuritySettings> securitySettings, IOptions<GlobalSettings> globalSettings)
: this(securitySettings, globalSettings, StaticServiceProvider.Instance.GetRequiredService<IBackOfficeExternalLoginProviders>())
{
}
[ActivatorUtilitiesConstructor]
public ConfigurationServerController(IOptions<SecuritySettings> securitySettings, IOptions<GlobalSettings> globalSettings, IBackOfficeExternalLoginProviders externalLoginProviders)
public ConfigurationServerController(IOptions<SecuritySettings> securitySettings, IOptions<GlobalSettings> globalSettings)
{
_securitySettings = securitySettings.Value;
_globalSettings = globalSettings.Value;
_externalLoginProviders = externalLoginProviders;
}
[AllowAnonymous]
[HttpGet("configuration")]
[MapToApiVersion("1.0")]
[ProducesResponseType(typeof(ServerConfigurationResponseModel), StatusCodes.Status200OK)]
@@ -47,8 +36,7 @@ public class ConfigurationServerController : ServerControllerBase
var responseModel = new ServerConfigurationResponseModel
{
AllowPasswordReset = _securitySettings.AllowPasswordReset,
VersionCheckPeriod = _globalSettings.VersionCheckPeriod,
AllowLocalLogin = _externalLoginProviders.HasDenyLocalLogin() is false,
VersionCheckPeriod = _globalSettings.VersionCheckPeriod
};
return Task.FromResult<IActionResult>(Ok(responseModel));
@@ -1,6 +1,5 @@
using System.Diagnostics;
using System.Linq.Expressions;
using System.Runtime.Versioning;
using System.Text;
using Asp.Versioning;
using Microsoft.AspNetCore.Http;
@@ -9,9 +8,7 @@ using Umbraco.Cms.Api.Management.ViewModels.Template.Query;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Models.PublishedContent;
using Umbraco.Cms.Core.Models.TemplateQuery;
using Umbraco.Cms.Core.PublishedCache;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Services.Navigation;
using Umbraco.Extensions;
namespace Umbraco.Cms.Api.Management.Controllers.Template.Query;
@@ -23,8 +20,6 @@ public class ExecuteTemplateQueryController : TemplateQueryControllerBase
private readonly IVariationContextAccessor _variationContextAccessor;
private readonly IPublishedValueFallback _publishedValueFallback;
private readonly IContentTypeService _contentTypeService;
private readonly IPublishedContentCache _contentCache;
private readonly IDocumentNavigationQueryService _documentNavigationQueryService;
private static readonly string _indent = $"{Environment.NewLine} ";
@@ -32,16 +27,12 @@ public class ExecuteTemplateQueryController : TemplateQueryControllerBase
IPublishedContentQuery publishedContentQuery,
IVariationContextAccessor variationContextAccessor,
IPublishedValueFallback publishedValueFallback,
IContentTypeService contentTypeService,
IPublishedContentCache contentCache,
IDocumentNavigationQueryService documentNavigationQueryService)
IContentTypeService contentTypeService)
{
_publishedContentQuery = publishedContentQuery;
_variationContextAccessor = variationContextAccessor;
_publishedValueFallback = publishedValueFallback;
_contentTypeService = contentTypeService;
_contentCache = contentCache;
_documentNavigationQueryService = documentNavigationQueryService;
}
[HttpPost("execute")]
@@ -60,7 +51,7 @@ public class ExecuteTemplateQueryController : TemplateQueryControllerBase
timer.Stop();
var contentTypeIconsByKey = _contentTypeService
.GetMany(results.Select(content => content.ContentType.Key).Distinct())
.GetAll(results.Select(content => content.ContentType.Key).Distinct())
.ToDictionary(contentType => contentType.Key, contentType => contentType.Icon);
return await Task.FromResult(Ok(new TemplateQueryResultResponseModel
@@ -118,13 +109,13 @@ public class ExecuteTemplateQueryController : TemplateQueryControllerBase
queryExpression.Append($".ChildrenOfType(\"{model.DocumentTypeAlias}\")");
return rootContent == null
? Enumerable.Empty<IPublishedContent>()
: rootContent.ChildrenOfType(_variationContextAccessor, _contentCache, _documentNavigationQueryService, model.DocumentTypeAlias);
: rootContent.ChildrenOfType(_variationContextAccessor, model.DocumentTypeAlias);
}
queryExpression.Append(".Children()");
return rootContent == null
? Enumerable.Empty<IPublishedContent>()
: rootContent.Children(_variationContextAccessor, _contentCache, _documentNavigationQueryService);
: rootContent.Children(_variationContextAccessor);
}
private IEnumerable<IPublishedContent> ApplyFiltering(IEnumerable<TemplateQueryExecuteFilterPresentationModel>? filters, IEnumerable<IPublishedContent> contentQuery, StringBuilder queryExpression)
@@ -38,10 +38,6 @@ public class TemplateControllerBase : ManagementApiControllerBase
.WithTitle("Master template not found")
.WithDetail("The master template referenced in the template was not found.")
.Build()),
TemplateOperationStatus.MasterTemplateCannotBeDeleted => BadRequest(problemDetailsBuilder
.WithTitle("Master template cannot be deleted")
.WithDetail("The master templates cannot be deleted. Please ensure the template is not a master template before you delete.")
.Build()),
_ => StatusCode(StatusCodes.Status500InternalServerError, problemDetailsBuilder
.WithTitle("Unknown template operation status.")
.Build()),
@@ -1,4 +1,4 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Api.Common.Builders;
using Umbraco.Cms.Api.Management.Routing;
@@ -17,6 +17,9 @@ public abstract class TemporaryFileControllerBase : ManagementApiControllerBase
.WithTitle("File extension not allowed")
.WithDetail("The file extension is not allowed.")
.Build()),
TemporaryFileOperationStatus.InvalidFileName => BadRequest(problemDetailsBuilder
.WithTitle("The provided file name is not valid")
.Build()),
TemporaryFileOperationStatus.KeyAlreadyUsed => BadRequest(problemDetailsBuilder
.WithTitle("Key already used")
.WithDetail("The specified key is already used.")
@@ -1,29 +0,0 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Core.Security.OperationStatus;
namespace Umbraco.Cms.Api.Management.Controllers.User.ClientCredentials;
[ApiExplorerSettings(GroupName = "User")]
public abstract class ClientCredentialsUserControllerBase : UserControllerBase
{
protected IActionResult BackOfficeUserClientCredentialsOperationStatusResult(BackOfficeUserClientCredentialsOperationStatus status) =>
OperationStatusResult(status, problemDetailsBuilder => status switch
{
BackOfficeUserClientCredentialsOperationStatus.InvalidUser => BadRequest(problemDetailsBuilder
.WithTitle("Invalid user")
.WithDetail("The specified user does not support this operation. Possibly caused by a mismatched client ID or an inapplicable user type.")
.Build()),
BackOfficeUserClientCredentialsOperationStatus.DuplicateClientId => BadRequest(problemDetailsBuilder
.WithTitle("Duplicate client ID")
.WithDetail("The specified client ID is already in use. Choose another client ID.")
.Build()),
BackOfficeUserClientCredentialsOperationStatus.InvalidClientId => BadRequest(problemDetailsBuilder
.WithTitle("Invalid client ID")
.WithDetail("The specified client ID is invalid. A valid client ID can only contain [a-z], [A-Z], [0-9], and [-._~]. Furthermore, including the prefix it cannot be longer than 255 characters.")
.Build()),
_ => StatusCode(StatusCodes.Status500InternalServerError, problemDetailsBuilder
.WithTitle("Unknown client credentials operation status.")
.Build()),
});
}
@@ -1,49 +0,0 @@
using Asp.Versioning;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Api.Management.ViewModels.User.ClientCredentials;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Security;
using Umbraco.Cms.Core.Security.Authorization;
using Umbraco.Cms.Core.Security.OperationStatus;
using Umbraco.Cms.Web.Common.Authorization;
using Umbraco.Extensions;
namespace Umbraco.Cms.Api.Management.Controllers.User.ClientCredentials;
[ApiVersion("1.0")]
public class CreateClientCredentialsUserController : ClientCredentialsUserControllerBase
{
private readonly IBackOfficeUserClientCredentialsManager _backOfficeUserClientCredentialsManager;
private readonly IAuthorizationService _authorizationService;
public CreateClientCredentialsUserController(
IBackOfficeUserClientCredentialsManager backOfficeUserClientCredentialsManager,
IAuthorizationService authorizationService)
{
_backOfficeUserClientCredentialsManager = backOfficeUserClientCredentialsManager;
_authorizationService = authorizationService;
}
[HttpPost("{id:guid}/client-credentials")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)]
public async Task<IActionResult> Create(CancellationToken cancellationToken, Guid id, CreateUserClientCredentialsRequestModel model)
{
AuthorizationResult authorizationResult = await _authorizationService.AuthorizeResourceAsync(
User,
UserPermissionResource.WithKeys(id),
AuthorizationPolicies.UserPermissionByResource);
if (!authorizationResult.Succeeded)
{
return Forbidden();
}
Attempt<BackOfficeUserClientCredentialsOperationStatus> result = await _backOfficeUserClientCredentialsManager.SaveAsync(id, model.ClientId, model.ClientSecret);
return result.Success
? Ok()
: BackOfficeUserClientCredentialsOperationStatusResult(result.Result);
}
}
@@ -1,48 +0,0 @@
using Asp.Versioning;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Security;
using Umbraco.Cms.Core.Security.Authorization;
using Umbraco.Cms.Core.Security.OperationStatus;
using Umbraco.Cms.Web.Common.Authorization;
using Umbraco.Extensions;
namespace Umbraco.Cms.Api.Management.Controllers.User.ClientCredentials;
[ApiVersion("1.0")]
public class DeleteClientCredentialsUserController : ClientCredentialsUserControllerBase
{
private readonly IBackOfficeUserClientCredentialsManager _backOfficeUserClientCredentialsManager;
private readonly IAuthorizationService _authorizationService;
public DeleteClientCredentialsUserController(
IBackOfficeUserClientCredentialsManager backOfficeUserClientCredentialsManager,
IAuthorizationService authorizationService)
{
_backOfficeUserClientCredentialsManager = backOfficeUserClientCredentialsManager;
_authorizationService = authorizationService;
}
[HttpDelete("{id:guid}/client-credentials/{clientId}")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)]
public async Task<IActionResult> Delete(CancellationToken cancellationToken, Guid id, string clientId)
{
AuthorizationResult authorizationResult = await _authorizationService.AuthorizeResourceAsync(
User,
UserPermissionResource.WithKeys(id),
AuthorizationPolicies.UserPermissionByResource);
if (!authorizationResult.Succeeded)
{
return Forbidden();
}
Attempt<BackOfficeUserClientCredentialsOperationStatus> result = await _backOfficeUserClientCredentialsManager.DeleteAsync(id, clientId);
return result.Success
? Ok()
: BackOfficeUserClientCredentialsOperationStatusResult(result.Result);
}
}
@@ -1,43 +0,0 @@
using Asp.Versioning;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Core.Security;
using Umbraco.Cms.Core.Security.Authorization;
using Umbraco.Cms.Web.Common.Authorization;
using Umbraco.Extensions;
namespace Umbraco.Cms.Api.Management.Controllers.User.ClientCredentials;
[ApiVersion("1.0")]
public class GetAllClientCredentialsUserController : ClientCredentialsUserControllerBase
{
private readonly IBackOfficeUserClientCredentialsManager _backOfficeUserClientCredentialsManager;
private readonly IAuthorizationService _authorizationService;
public GetAllClientCredentialsUserController(
IBackOfficeUserClientCredentialsManager backOfficeUserClientCredentialsManager,
IAuthorizationService authorizationService)
{
_backOfficeUserClientCredentialsManager = backOfficeUserClientCredentialsManager;
_authorizationService = authorizationService;
}
[HttpGet("{id:guid}/client-credentials")]
[ProducesResponseType(typeof(IEnumerable<string>), StatusCodes.Status200OK)]
public async Task<IActionResult> GetAll(CancellationToken cancellationToken, Guid id)
{
AuthorizationResult authorizationResult = await _authorizationService.AuthorizeResourceAsync(
User,
UserPermissionResource.WithKeys(id),
AuthorizationPolicies.UserPermissionByResource);
if (!authorizationResult.Succeeded)
{
return Forbidden();
}
IEnumerable<string> clientIds = await _backOfficeUserClientCredentialsManager.GetClientIdsAsync(id);
return Ok(clientIds);
}
}
@@ -128,10 +128,6 @@ public abstract class UserOrCurrentUserControllerBase : ManagementApiControllerB
.WithTitle("Self password reset not allowed")
.WithDetail("It is not allowed to reset the password for the account you are logged in to.")
.Build()),
UserOperationStatus.InvalidUserType => BadRequest(problemDetailsBuilder
.WithTitle("Invalid user type")
.WithDetail("The target user type does not support this operation.")
.Build()),
UserOperationStatus.Forbidden => Forbidden(),
_ => StatusCode(StatusCodes.Status500InternalServerError, problemDetailsBuilder
.WithTitle("Unknown user operation status.")
@@ -1,34 +0,0 @@
using Asp.Versioning;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Api.Management.Factories;
using Umbraco.Cms.Api.Management.ViewModels.Webhook;
using Umbraco.Cms.Api.Management.ViewModels.Webhook.Logs;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Services;
namespace Umbraco.Cms.Api.Management.Controllers.Webhook.Logs;
[ApiVersion("1.0")]
public class AllWebhookLogController : WebhookLogControllerBase
{
private readonly IWebhookLogService _webhookLogService;
private readonly IWebhookPresentationFactory _webhookPresentationFactory;
public AllWebhookLogController(IWebhookLogService webhookLogService, IWebhookPresentationFactory webhookPresentationFactory)
{
_webhookLogService = webhookLogService;
_webhookPresentationFactory = webhookPresentationFactory;
}
[HttpGet("logs")]
[MapToApiVersion("1.0")]
[ProducesResponseType(typeof(WebhookResponseModel), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
public async Task<IActionResult> Logs(CancellationToken cancellationToken, int skip = 0, int take = 100)
{
PagedModel<WebhookLog> logs = await _webhookLogService.Get(skip, take);
IEnumerable<WebhookLogResponseModel> logResponseModels = logs.Items.Select(x => _webhookPresentationFactory.CreateResponseModel(x));
return Ok(logResponseModels);
}
}
@@ -1,9 +0,0 @@
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Api.Management.Routing;
using Umbraco.Cms.Core;
namespace Umbraco.Cms.Api.Management.Controllers.Webhook.Logs;
[VersionedApiBackOfficeRoute($"{Constants.UdiEntityType.Webhook}")]
[ApiExplorerSettings(GroupName = "Webhook")]
public class WebhookLogControllerBase : ManagementApiControllerBase;
@@ -81,7 +81,6 @@ internal static class BackOfficeAuthPolicyBuilderExtensions
AddAllowedApplicationsPolicy(AuthorizationPolicies.TreeAccessDocuments, Constants.Applications.Content);
AddAllowedApplicationsPolicy(AuthorizationPolicies.TreeAccessDocumentsOrDocumentTypes, Constants.Applications.Content, Constants.Applications.Settings);
AddAllowedApplicationsPolicy(AuthorizationPolicies.TreeAccessDocumentOrMediaOrContentTypes, Constants.Applications.Content, Constants.Applications.Settings, Constants.Applications.Media);
AddAllowedApplicationsPolicy(AuthorizationPolicies.TreeAccessDocumentsOrMediaOrMembersOrContentTypes, Constants.Applications.Content, Constants.Applications.Media, Constants.Applications.Members, Constants.Applications.Settings);
AddAllowedApplicationsPolicy(AuthorizationPolicies.TreeAccessDocumentTypes, Constants.Applications.Settings);
AddAllowedApplicationsPolicy(AuthorizationPolicies.TreeAccessLanguages, Constants.Applications.Settings);
AddAllowedApplicationsPolicy(AuthorizationPolicies.TreeAccessMediaTypes, Constants.Applications.Settings);
@@ -10,7 +10,7 @@ internal static class MemberBuilderExtensions
{
internal static IUmbracoBuilder AddMember(this IUmbracoBuilder builder)
{
builder.Services.AddSingleton<IMemberPresentationFactory, MemberPresentationFactory>();
builder.Services.AddTransient<IMemberPresentationFactory, MemberPresentationFactory>();
builder.Services.AddTransient<IMemberEditingPresentationFactory, MemberEditingPresentationFactory>();
builder.WithCollectionBuilder<MapDefinitionCollectionBuilder>().Add<MemberMapDefinition>();

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