Compare commits

..
159 Commits
Author SHA1 Message Date
Andy Butland c6ab394947 Merge branch 'release/10.8.11' into v10/dev
# Conflicts:
#	version.json
2025-07-02 06:32:18 +02:00
Kenn JacobsenandGitHub d8f68d2c40 Merge commit from fork 2025-06-24 08:39:17 +02:00
Andy Butland ff88617db0 Bumped version to 10.8.11. 2025-06-05 07:49:09 +02:00
Andy Butland 9f912aea0e Merge branch 'release/10.8.10' into v10/dev
# Conflicts:
#	version.json
2025-05-06 07:15:10 +02:00
Andy Butland ba95c12f09 Resolved failures in unit tests. 2025-05-06 05:37:30 +02:00
Andy ButlandandGitHub 14fbd20665 Merge commit from fork
* Backport user enumeration fix.

* Supress warning on use of .NET 6 with TimeProvider dependency.

* Replace TimeProvider with Stopwatch, as the former isn't tested against .NET 6.0 and generates warnings.

* Remove full path details from exception when requesting a path outside of the physical file system's root.

* Added randomness to login duration.

* Ensured against negative duration.
2025-05-06 05:11:03 +02:00
mole 2d8b5e8786 Use windows agent for nuget push 2025-04-28 10:48:26 +02:00
Andy Butland 747e095178 Bump version to 10.8.10. 2025-04-22 10:06:27 +02:00
Andy Butland 1cfa5a225e Merge branch 'release/10.8.9' into v10/dev
# Conflicts:
#	version.json
2025-03-11 09:20:51 +01:00
7888b9a4ce Merge commit from fork
* Bumped version to 10.8.9.

* Fixed parsing of node if in content and media permission querystring handlers to retrieve expected value when multiple are provided in the querystring.

# Conflicts:
#	tests/Umbraco.Tests.UnitTests/Umbraco.Web.BackOffice/Authorization/ContentPermissionsQueryStringHandlerTests.cs
#	tests/Umbraco.Tests.UnitTests/Umbraco.Web.BackOffice/Authorization/MediaPermissionsQueryStringHandlerTests.cs

* Add HttpPost attributes to backoffice endpoints that should only accept post requests.

* Narrow PermissionQueryString parsing to the releveant UmbracoObjectType

* Add missed update from v10

---------

Co-authored-by: Sven Geusens <sge@umbraco.dk>
2025-03-11 05:11:08 +01:00
Andy Butland e31582b297 Backport bumped imagesharp to prevent CVE-2025-27598 #18602 2025-03-09 08:58:25 +01:00
Nikolaj Geisle 75cc017a18 Merge branch 'release/10.8.8' into v10/dev
# Conflicts:
#	version.json
2025-01-23 11:08:09 +01:00
d60137e6da Merge commit from fork
* Ensure preview can only be requested with a valid culture code.

* Update src/Umbraco.Web.BackOffice/Controllers/PreviewController.cs

Co-authored-by: Nikolaj Geisle <70372949+Zeegaan@users.noreply.github.com>

* Restricted to predefined culture codes.

---------

Co-authored-by: Nikolaj Geisle <70372949+Zeegaan@users.noreply.github.com>
2025-01-20 14:14:28 +01:00
Andy Butland 9f9c88781a Bumped version to 10.8.8. 2025-01-07 10:08:01 +01:00
c7014e159b Sort manifest file paths alphabetically (#14466)
* Sort manifest file paths alphabetically

* Update src/Umbraco.Infrastructure/Manifest/ManifestParser.cs

Co-authored-by: Ronald Barendse <ronald@barend.se>

---------

Co-authored-by: Ronald Barendse <ronald@barend.se>
2024-11-21 16:19:48 +01:00
Zeegaan 31e1acce67 Merge remote-tracking branch 'origin/release/10.8.7' into v10/dev
# Conflicts:
#	version.json
2024-10-23 08:48:08 +02:00
Sven GeusensandNikolaj Geisle 35c51a029a Prevents XSS when viewing an uploaded SVG
from the media-info and image-preview components.
2024-10-21 12:42:06 +02:00
ElitsaandNikolaj Geisle 8c1128c85b Update the HttpContext's user with the authenticated user's principal 2024-10-21 12:41:42 +02:00
ElitsaandNikolaj Geisle c9021ab2d2 Reduce the time when getRemainingTimeoutSeconds request is made from 30s to 20s, so fewer calls are made 2024-10-21 12:41:18 +02:00
ElitsaandNikolaj Geisle 67a71f8f82 Make sure that the client shows the login screen as close to the server's timout time as possible 2024-10-21 12:41:17 +02:00
Andreas ZerbstandAndreas Zerbst edd0a4a4a9 Updated message pack (#17320)
(cherry picked from commit 11270eaaf5)
2024-10-21 11:46:43 +02:00
Andreas ZerbstandGitHub 11270eaaf5 Updated message pack (#17320) 2024-10-21 11:00:47 +02:00
Bjarke BergandGitHub 2d71b5a63b Updated image sharp to a non vulnerable version (#17290) 2024-10-16 12:16:38 +02:00
Nikolaj Geisle 9bab74d30e Bump version.json 2024-10-07 11:55:34 +02:00
Bjarke Berg 0ee0db8071 Merge remote-tracking branch 'refs/remotes/origin/release/10.8' into v10/dev
# Conflicts:
#	version.json
2024-05-21 08:44:27 +02:00
Bjarke BergandGitHub c17d4e1a60 Merge pull request from GHSA-j74q-mv2c-rxmp 2024-05-17 08:37:51 +02:00
Bjarke Berg fee222daff Bump version 2024-05-03 09:29:37 +02:00
119fde2033 V10: Fix for fallback file upload (#14892) (#15868)
* Fix for fallback file upload (#14892)

* Added check for file type

* Removed unneeded null checks and fixed tabs

* Cleaning

* Cleanups, cleanups, and removal of unneeded null checks

* Reverted removal of relationshipservice

* Revert null check removals (too risky)

---------

Co-authored-by: Ambert van Unen <AvanUnen@ilionx.com>
Co-authored-by: Laura Neto <12862535+lauraneto@users.noreply.github.com>

(cherry picked from commit 0b5d1f8aa6)

* Fix up formatting

---------

Co-authored-by: Ambert van Unen <ambertvu@gmail.com>
2024-04-24 13:44:20 +02:00
Andy ButlandandGitHub 52c21b0fca Updates JSON schema for Umbraco 10 with latest references for Forms and Deploy (#15918) 2024-03-20 13:20:40 +01:00
Bjarke Berg 99d5a7e609 Merge remote-tracking branch 'origin/release/10.8' into v10/dev 2024-03-19 08:43:59 +01:00
JeyandBjarke Berg 7e1d1a1968 Merge pull request from GHSA-552f-97wf-pmpq
Co-authored-by: jey <jey@umbraco.dk>
(cherry picked from commit b743f6a2df)
2024-03-18 08:28:25 +01:00
b743f6a2df Merge pull request from GHSA-552f-97wf-pmpq
Co-authored-by: jey <jey@umbraco.dk>
2024-03-18 08:27:41 +01:00
Bjarke Berg a2511ff09b Fixing locking issues for document type saves. (#15854)
* Added  ExecuteNonQuery(DbCommand command) on database to ensure we call OnExecutingCommand and OnExecutedCommand when executing DbCommands

* Added Cache Instructions lock, to avoid deadlocks

* Optimized read locks for nucache when only one content type is rebuilt

* Optimized the SqlServer locks, so only one command is executed (and thereby roundtrip) per lock instead of two

* Avoid breaking changes

* Cosmetic changes

* Take locks if everything is rebuild

* Use same lock in scopes, to avoid potential deadlocks between the two

* Use eager locks in PublishedSnapshotService.cs

* Added timeouts to some of the application locks

* Revert "Use eager locks in PublishedSnapshotService.cs"

This reverts commit 01873aae97.

* Revert "Added Cache Instructions lock, to avoid deadlocks"

This reverts commit e3fca7c12a.

* Use single readlock call to lock many

* Use eager locks for reads

* Eager write locks

* Ignore test of lazy locks

* Unique timeout exception messages

---------

Co-authored-by: kjac <kja@umbraco.dk>
(cherry picked from commit 2c23e67c65)
2024-03-15 14:55:20 +01:00
2c23e67c65 Fixing locking issues for document type saves. (#15854)
* Added  ExecuteNonQuery(DbCommand command) on database to ensure we call OnExecutingCommand and OnExecutedCommand when executing DbCommands

* Added Cache Instructions lock, to avoid deadlocks

* Optimized read locks for nucache when only one content type is rebuilt

* Optimized the SqlServer locks, so only one command is executed (and thereby roundtrip) per lock instead of two

* Avoid breaking changes

* Cosmetic changes

* Take locks if everything is rebuild

* Use same lock in scopes, to avoid potential deadlocks between the two

* Use eager locks in PublishedSnapshotService.cs

* Added timeouts to some of the application locks

* Revert "Use eager locks in PublishedSnapshotService.cs"

This reverts commit 01873aae97.

* Revert "Added Cache Instructions lock, to avoid deadlocks"

This reverts commit e3fca7c12a.

* Use single readlock call to lock many

* Use eager locks for reads

* Eager write locks

* Ignore test of lazy locks

* Unique timeout exception messages

---------

Co-authored-by: kjac <kja@umbraco.dk>
2024-03-15 10:56:02 +01:00
Bjarke Berg 8e837d387d Merge remote-tracking branch 'origin/v10/dev' into release/10.8
# Conflicts:
#	version.json
2024-03-14 14:53:42 +01:00
Bjarke Berg 801fb5f885 Bump version 2024-03-14 14:48:09 +01:00
Bjarke BergandGitHub ed517ecd86 Update Imagesharp (#15885) 2024-03-14 14:31:30 +01:00
Bjarke Berg b5e46ba880 Merge remote-tracking branch 'origin/release/10.8' into v10/dev 2024-02-06 11:07:16 +01:00
Nikolaj GeisleandGitHub cbf9f9bcd1 Merge pull request from GHSA-gvpc-3pj6-4m9w
* Add MarkDownPropertyValueEditor with html sanitizer

* Implement IMarkdownSanitizer.
2024-02-06 09:53:40 +01:00
Bjarke Berg 2735f17ed8 Merge remote-tracking branch 'origin/release/10.8' into v10/dev
# Conflicts:
#	version.json
2024-02-01 11:09:47 +01:00
Ronald BarendseandBjarke Berg e37cf30690 Skip cache refresher operations for content blueprints (#15633)
* Skip cache refresher operations for content blueprints

* Fix JsonPayload deserialization error by adding a default constructor and property initializers

* Obsolete JsonPayload constructor and update usages

(cherry picked from commit 3e28e10cdf)
2024-02-01 10:00:42 +01:00
Bjarke BergandGitHub 118ac8e230 V10+ version of https://github.com/umbraco/Umbraco-CMS/pull/15638 (#15664) 2024-02-01 09:58:42 +01:00
Ronald BarendseandGitHub 3e28e10cdf Skip cache refresher operations for content blueprints (#15633)
* Skip cache refresher operations for content blueprints

* Fix JsonPayload deserialization error by adding a default constructor and property initializers

* Obsolete JsonPayload constructor and update usages
2024-02-01 09:55:09 +01:00
AleksanderandBjarke Berg b18b6cc5e7 Pass cache level to properties when creating published content in nucache
(cherry picked from commit d9d2b66e85)

# Conflicts:
#	src/Umbraco.Web/PublishedCache/NuCache/PublishedContent.cs
(cherry picked from commit 040495f359)
2024-01-31 10:52:01 +01:00
Elitsa MarinovskaandBjarke Berg 7a04baf9ef V10: Pass in variation context to published cache (#15563)
* Make sure that we always have variation context

* Fix references

(cherry picked from commit 57b3a196bf)
2024-01-30 15:57:33 +01:00
Sven GeusensandBjarke Berg bc1ddaed8c Run the same cleanup with scaffolding content as when copying. (#15541)
* Run the same cleanup with scaffolding content as when copying.

- Added a new ContentScaffoldedNotification
- Published the notification when a new scaffold has been created from a blueprint (content template)
- Linked up the ComplextPEContent handler to do the same cleanup for the new notification as when copying.
- registered handlers to the event for blocklist, blockgrid and nested content

* PR pattern matching suggestion

Co-authored-by: Nikolaj Geisle <70372949+Zeegaan@users.noreply.github.com>

---------

Co-authored-by: Sven Geusens <sge@umbraco.dk>
Co-authored-by: Nikolaj Geisle <70372949+Zeegaan@users.noreply.github.com>
(cherry picked from commit dff90c6ec0)
2024-01-30 15:54:33 +01:00
Bjarke Berg 860afb35f2 Bump version 2024-01-30 15:53:21 +01:00
Ronald BarendseandGitHub 9799c550f4 Create shadow file systems in configured LocalTempPath (#15637) 2024-01-29 08:48:45 +01:00
Ronald BarendseandGitHub b7e43a8def Get references from macro parameters using IDataValueReferenceFactory (#15625) 2024-01-23 16:08:17 +01:00
Ronald BarendseandGitHub cdbbd6a921 Optimize relation tracking for adding new and keeping existing relations (#15596)
* Include automatic relation type aliases from factory and fix SQL parameter overflow (#15141)

* Include automatic relation type aliases from factory

* Remove unnessecary distinct and fix SQL parameter overflow issue

* Fixed assertions and test distinct aliases

* Simplified collection assertions

* Improve logging of invalid reference relations (#15160)

* Include automatic relation type aliases from factory

* Remove unnessecary distinct and fix SQL parameter overflow issue

* Fixed assertions and test distinct aliases

* Simplified collection assertions

* Improve logging of invalid reference relations

* Always get all automatic relation type aliases

* Do not set relation type alias for unknown entity types

* Get references from recursive (nested/block) properties

* Optimize relation tracking for adding new and keeping existing relations

* Optimize getting references by grouping by property editor alias and avoiding duplicate parsing of the same value
2024-01-22 14:53:20 +01:00
Ronald BarendseandGitHub 5198e7c52d Backport relation tracking fixes and get references from recursive (nested/block) properties (#15593)
* Include automatic relation type aliases from factory and fix SQL parameter overflow (#15141)

* Include automatic relation type aliases from factory

* Remove unnessecary distinct and fix SQL parameter overflow issue

* Fixed assertions and test distinct aliases

* Simplified collection assertions

* Improve logging of invalid reference relations (#15160)

* Include automatic relation type aliases from factory

* Remove unnessecary distinct and fix SQL parameter overflow issue

* Fixed assertions and test distinct aliases

* Simplified collection assertions

* Improve logging of invalid reference relations

* Always get all automatic relation type aliases

* Do not set relation type alias for unknown entity types

* Get references from recursive (nested/block) properties
2024-01-19 20:02:57 +01:00
Andy ButlandandGitHub 49f5d2e2d4 Updates JSON schema for Umbraco 10 to include details of additional configuration introduced in Forms and Deploy. (#15566) 2024-01-12 13:13:20 +01:00
Elitsa MarinovskaandGitHub 57b3a196bf V10: Pass in variation context to published cache (#15563)
* Make sure that we always have variation context

* Fix references
2024-01-11 12:46:31 +01:00
dff90c6ec0 Run the same cleanup with scaffolding content as when copying. (#15541)
* Run the same cleanup with scaffolding content as when copying.

- Added a new ContentScaffoldedNotification
- Published the notification when a new scaffold has been created from a blueprint (content template)
- Linked up the ComplextPEContent handler to do the same cleanup for the new notification as when copying.
- registered handlers to the event for blocklist, blockgrid and nested content

* PR pattern matching suggestion

Co-authored-by: Nikolaj Geisle <70372949+Zeegaan@users.noreply.github.com>

---------

Co-authored-by: Sven Geusens <sge@umbraco.dk>
Co-authored-by: Nikolaj Geisle <70372949+Zeegaan@users.noreply.github.com>
2024-01-10 12:22:36 +01:00
AleksanderandSebastiaan Janssen 040495f359 Pass cache level to properties when creating published content in nucache
(cherry picked from commit d9d2b66e85)

# Conflicts:
#	src/Umbraco.Web/PublishedCache/NuCache/PublishedContent.cs
2024-01-08 14:42:17 +01:00
Bjarke Berg a2ad95d965 Change content 2023-12-22 13:49:57 +01:00
Bjarke Berg 64f2447c0e Added script to update default version 2023-12-22 13:15:13 +01:00
Bjarke Berg b648126d19 Use wildcard as default valid 2023-12-22 12:59:13 +01:00
812b414d96 Batched more update calls to avoid Sql paramater count error (#15487)
Co-authored-by: Sven Geusens <sge@umbraco.dk>
2023-12-21 09:43:37 +01:00
Bjarke Berg ec91c47158 Merge remote-tracking branch 'origin/release/10.8' into v10/dev
# Conflicts:
#	version.json
2023-12-20 09:51:14 +01:00
Kenn JacobsenandGitHub e04a41be45 Add policies for content template handling (#15482) 2023-12-19 14:18:48 +01:00
Chris Fitz-AvonandBjarke Berg d5156aced2 Remove content section access policy from GetAllLanguages endpoint. #15435 (#15450)
(cherry picked from commit cedfdcc9b2)
2023-12-18 08:25:00 +01:00
Bjarke Berg fa9b0e4718 Bump version 2023-12-18 08:23:34 +01:00
Bjarke Berg 8915064780 Merge remote-tracking branch 'origin/release/10.8' into v10/dev
# Conflicts:
#	version.json
2023-12-13 09:28:19 +01:00
ZeegaanandBjarke Berg 677ff59d04 Loosen up policies so editors can get what they need 2023-12-12 15:46:46 +01:00
Bjarke Berg 1fe2ec08be bump version 2023-12-12 15:35:15 +01:00
Bjarke Berg 5dfa76600a Post fix 2023-12-11 16:32:38 +01:00
Bjarke Berg 83bcd37250 Merge remote-tracking branch 'origin/release/10.8' into v10/dev 2023-12-11 14:37:10 +01:00
Kenn JacobsenandGitHub 4a7ad4a562 Merge pull request from GHSA-v98m-398x-269r 2023-12-11 14:00:23 +01:00
cdd4d2a000 Merge pull request from GHSA-cfr5-7p54-4qg8
* Bump version

* Apply authorization policies to controllers

* Return bad request if we urltracking is disabled

* Apply authorization policies to controllers

* Return bad request if we urltracking is disabled

---------

Co-authored-by: Bjarke Berg <mail@bergmania.dk>
Co-authored-by: Zeegaan <nge@umbraco.dk>
2023-12-11 13:59:59 +01:00
be5a740c96 Merge pull request from GHSA-8qp8-9rpw-j46c
* Added Exception handling and replicated error and info message

* Update auth.resource.js

Fixed the message

* Changed Delay introduction to early phase to avoid repeating code.

---------

Co-authored-by: jey <jey@umbraco.dk>
Co-authored-by: Jey <cyaqublu@gmail.com>
2023-12-11 13:59:35 +01:00
13cc320f19 Merge pull request from GHSA-6324-52pr-h4p5
* Bump version

* Fix https://github.com/umbraco/Umbraco-CMS/security/advisories/GHSA-6324-52pr-h4p5

* Fix https://github.com/umbraco/Umbraco-CMS/security/advisories/GHSA-6324-52pr-h4p5

---------

Co-authored-by: Bjarke Berg <mail@bergmania.dk>
Co-authored-by: Zeegaan <nge@umbraco.dk>
2023-12-11 13:59:03 +01:00
ZeegaanandBjarke Berg b5544aa520 Suppress flow when queueing background threads 2023-12-11 12:43:25 +01:00
Bjarke Berg 9e3f8c7d0c Merge remote-tracking branch 'origin/release/10.8' into v10/dev
# Conflicts:
#	version.json
2023-12-08 13:12:33 +01:00
Bjarke Berg cb76554455 Bugfix: Count existing media correct (#15206)
* https://github.com/umbraco/Umbraco-CMS/issues/15205
Count media correct

* Fix content should be document

(cherry picked from commit d2ff2ea46e)
2023-12-07 09:21:25 +01:00
Jacob Overgaard 37e4d80ce8 Update nightly build (#15359)
* cherry-pick templateParameters from v14/dev

* add isNightly parameter

* remove v9

* add parameter check for myget isNightly
2023-12-05 12:45:23 +01:00
Bjarke Berg d5d4edbe92 Bump Version 2023-11-30 21:51:54 +01:00
Bjarke Berg 8d9343b564 Merge branch 'release/10.8' into v10/dev
# Conflicts:
#	version.json
2023-11-27 18:10:40 +01:00
Bjarke Berg 1ee891c8d5 Merge remote-tracking branch 'origin/release/10.8' into release/10.8 2023-11-27 18:09:41 +01:00
Bjarke Berg bf1a5b9b28 Bump version 2023-11-27 18:09:30 +01:00
Bjarke Berg 696b74cdb6 Merge branch 'release/10.8' into v10/dev 2023-11-27 18:08:30 +01:00
Bjarke BergandGitHub bf8af4588e Centralised NuGet packages (#15293)
* Moved to centralized nuget packages

* Handle JsonSchema

* Change PackageReference to PackageVersion

* Opt out of central package management for mangement api (at least for versions less than 14) and the UI project

* ManagePackageVersionsCentrally = false

* Change folder acceptance tests are build, to avoid reading the test Directory.Packages.props

* Change folder acceptance tests are build, to avoid reading the test Directory.Packages.props

* change working directory

* workingDirectory

* Force the template to use local nuget packages

* Force the template to use local nuget packages

* clean up

* added wait on

* added wait on
2023-11-27 15:18:00 +01:00
Kenn JacobsenandBjarke Berg 4781fb1d8b Ensure content types are updated in ContentStore when a data type changes (#15239) 2023-11-17 10:37:23 +01:00
Bjarke Berg d714741bee Fixed build 2023-11-15 09:53:28 +01:00
Bjarke Berg ea33b73daa Added scope.complete in edge cases. (#15207)
(cherry picked from commit 28e8ac11f0)
2023-11-15 09:46:24 +01:00
Bjarke Berg ba64b46824 Merge remote-tracking branch 'origin/release/10.8' into release/10.8 2023-11-15 09:45:30 +01:00
Bjarke Berg 04e59e9a9d Fixed a couple of occurrences where scopes was auto-completed while modified db state (#14947)
* Fixed a couple of occurrences where scopes was auto-complated while actually modified the state of the database.

* Added temp ctor to fix boot issue

(cherry picked from commit 41f8f03c2c)
2023-11-15 09:45:23 +01:00
Jacob OvergaardandGitHub be39b9c19a V10: Update @umbraco-ui/uui, jQuery, ace-editor, nouislider, spectrum-colorpicker2 (#15197)
* V13: Add support for Node.js 20 (#15022)

* update versionSpec to nodejs 20 and avoid specifying minor and patch versions to improve Azure Pipelines stability

* update engines and package lock files

* update helpful displayNames for npm builds

* add retry to counter nodejs.org timeouts

add retryCountOnTaskFailure as suggested on https://developercommunity.visualstudio.com/t/NodeTool0-task-to-install-nodejs-fails/10455454

* update lockfile

* bump nvm support to latest node.js 20.9 LTS

* build(deps): bump @umbraco-ui/uui from 1.3.0 to 1.5.0

* build(deps): bump public dependencies to latest

* build(deps-dev): bump build dependencies to latest

* build(deps-dev): bump test tools to latest

* build: add script to lint and run --fix

* revert login merge from v13
2023-11-14 13:08:29 +01:00
Bjarke Berg 9a143c0850 Merge remote-tracking branch 'origin/release/10.8' into release/10.8 2023-11-14 11:46:26 +01:00
Bjarke Berg df4cd63962 https://github.com/umbraco/Umbraco-CMS/issues/15195 Fixed issue with media not cached correct (#15196) 2023-11-14 10:59:30 +01:00
Bjarke Berg a17e398f28 Merge remote-tracking branch 'origin/release/10.8' into v10/dev 2023-11-14 09:29:36 +01:00
Bjarke Berg 9af9dd2c02 Merge remote-tracking branch 'origin/release/10.8' into v10/dev 2023-11-14 09:29:09 +01:00
Bjarke BergandGitHub 2266a98fc6 https://github.com/umbraco/Umbraco-CMS/issues/15195 Fixed issue with media not cached correct (#15196) 2023-11-14 09:14:01 +01:00
151fccee97 Performance optimisations related to blocks (and multi url picker) (#15184)
* Added content types to property index value factory, because the deep cloning is too expensive to execute often

* Do not use entityservice in ToEditor, as it goes to the database. We need to use something that is cached.

* Small performance optimization for nested content too

* A little formatting and an obsoletion message

---------

Co-authored-by: kjac <kja@umbraco.dk>
2023-11-14 08:12:05 +01:00
Bjarke Berg abfa07367f Bump version 2023-11-13 18:06:41 +01:00
Bjarke BergandGitHub e5de8bf56b Updated nuget packages (#15192) 2023-11-13 14:59:52 +01:00
Bjarke Berg 00bb5170a0 Merge remote-tracking branch 'origin/v10/dev' into release/10.8
# Conflicts:
#	version.json
2023-11-09 13:53:46 +01:00
919138c299 #14835 Update link on permissionsreport.html (#14872)
* #14835 Update link on permissionsreport.html

Updated link to permissions page to point to 10-LTS documentation

* retrigger checks

---------

Co-authored-by: Rasmus Berntsen <rasmus.berntsen@isobar.com>
2023-11-09 13:53:18 +01:00
Ronald BarendseandGitHub ed65645a33 Add global.json and cleanup UI project file (#14900)
* Add global.json with minimum .NET 6.0.5 SDK version and roll forward policy

* Clean up UI project file
2023-11-09 13:52:21 +01:00
Ronald BarendseandGitHub d0be4ab263 Publish ContentTreeChangeNotification after saving/deleting blueprints (#14898) 2023-11-09 13:51:05 +01:00
Bjarke Berg ddb26632f9 Bump to rc 2023-11-09 13:38:34 +01:00
Bjarke Berg d5b1f5875b Bump version 2023-11-09 13:38:13 +01:00
Bjarke Berg f8c1a560ff Bump version 2023-11-09 13:36:57 +01:00
9e5ff305c8 Batched bulk WhereIn query to avoid To mana paramaters error (#15004)
Co-authored-by: Sven Geusens <sge@umbraco.dk>
2023-11-07 10:49:46 +01:00
Elitsa MarinovskaandGitHub ffaa77eb25 Update correct action (#15142) 2023-11-06 17:54:16 +01:00
Kenn JacobsenandGitHub 6777145079 Ensure that missing access rules do not break the site (#15081) 2023-11-02 08:45:27 +01:00
Elitsa MarinovskaandGitHub 92af8ac881 Check content permissions before performing action (#15043)
* Setting actionContext.Result when authz wasn't successful

* Taking into account permissions when it is a new node

* Cleanup

* Passing nodeId as path when new item
2023-11-01 11:30:32 +01:00
Lucas Bach BisgaardandSebastiaan Janssen bde88caf14 Fixes #14351 - Using Fallback to default langauge on a specific item changes the whole VariationContext (#14620)
(cherry picked from commit 738749b705)
2023-10-26 11:47:44 +02:00
Bjarke BergandGitHub f4ac6cdded Added "pr: none" to nightly trigger (#15044) 2023-10-26 09:01:25 +02:00
Elitsa MarinovskaandGitHub 132935c2d7 Handle corrupted index (#14889) 2023-10-09 10:19:36 +02:00
Kenn JacobsenandGitHub 04b05714e5 Add notification state to "final" published notification when publishing with descendants (#14922)
* Revert "V10/bugfix/14543 publish descendants (#14763)"

This reverts commit f750bca453.

* Add notification state to "final" published notification when publishing with descendants
2023-10-05 14:39:00 +02:00
Kenn JacobsenandGitHub 7be1852827 Make sure to sign out external users if an error occurs during sign-in (#14867) 2023-09-28 13:25:11 +02:00
Andreas ZerbstandGitHub 158cef1692 V10 fixed up failing acceptance test (#14829)
* Updated playwright, our builders, and our helpers

* Updated the use of our testhelper methods, we removed the need for sending umbracoApi through the methods as a parameter since it was unnecessary

* Added a timeout

* Updated successNotifications and added a locator

* Removed timeout from successNotification

* Added additional timeouts and updated old locators which were failing

* Updated locator so it only finds one element

* Removed timeouts and added a waitForTimeout

* Removed timeouts

* Removed timeouts

* Added timeouts

* Updated locator

* Added a timeout

* Updated locators for clicking button

* Changed locator for adding the image, sometimes the old locator would find 2 locators
2023-09-20 12:15:24 +02:00
Bjarke Berg 1a4f3981eb Merge remote-tracking branch 'origin/v10/dev' into v10/dev 2023-09-20 09:42:28 +02:00
Bjarke Berg 7765f98ee1 Added random delay in forgot password to lessen the risk the response times can be use to determine whether the mail exist or not. 2023-09-20 09:02:32 +02:00
6b72880c0b fix: ensured that allowed user name characters are supported in backoffice users as well (#14810) (#14825)
Co-authored-by: Arkadiusz Biel <bielu@bielu.com.pl>
2023-09-19 09:01:35 +02:00
Andy ButlandandGitHub d9f3936a43 Updated dependencies to Forms and Deploy for JSON schema generation. (#14826) 2023-09-19 08:54:41 +02:00
bf09c87f87 Mention MultiUrlPickerValueConverter instead (#14818)
Co-authored-by: Zeegaan <nge@umbraco.dk>
2023-09-18 13:48:11 +02:00
Sven GeusensandGitHub ec2134dea0 Disabled nighly build branch trigger (#14807) 2023-09-15 10:02:29 +02:00
Nikolaj GeisleandGitHub 8ccc2faee0 Add UsePagedSqlQuery setting to NucacheSettings (#14806)
* imlement UsePagedSqlQuery

* Remove unneccesary spacing
2023-09-14 20:11:08 +02:00
Jacob Overgaard 0a4cc27a5d V12: Property editors should only contain layout when in preview mode (#14796)
* Revert "Make sure the property editor layout is contained within its container (prevent Z-index bleed-through) (#13583)"

This reverts commit 8463d906a7.

* remove seemingly unused attribute readonly

* contain everything with contain:layout

this is set if the property editor is in preview mode to make sure all overlays work

* Revert "remove seemingly unused attribute readonly"

This reverts commit 7a2743e8f6.
2023-09-14 13:19:36 +02:00
c8e054baa2 V10/feature/pipeline test filters (#14766)
* Add configuration/code to not run certain tests based on variables/release builds

* Applied longrunning testAttribute to the worst offenders (>200ms on my machine)

* Fix yaml notation

* split up windows/non windows test runs

* Added supression for moved tests

* Fix yaml validation issues

* Change yaml string parameter null value to empty string

* Convert empty strings to whitespace strings

* Rename and cleanup some paramater to better reflect why we use them

* Nightly build test

* Change nightly build authentication type

* template paramater fix

* Update nightly pipeline name

---------

Co-authored-by: Sven Geusens <sge@umbraco.dk>
2023-09-12 14:16:27 +02:00
Bjarke Berg 95ffdae475 Merge remote-tracking branch 'origin/v10/dev' into v10/dev 2023-09-12 09:43:52 +02:00
Andreas ZerbstandJacob Overgaard ca8ac21f4b Added test for checking if settings model contains default value (#14513)
* Added test for checking if a settings model contains default value.
Fixed tests

* Bumped version

* Moved createSliderWithDefaultValue function to our testHelpers. Removed the use of count in test since there was only one
2023-09-11 15:13:08 +02:00
Jacob Overgaard f85a0d1715 Render all tabs in the background to ensure they register their initial state on any $scope.model in the stack. (#14493)
Sometimes a tab, e.g. a "block list settings model" might register some default values, but if the user never clicks on the tab then they are not registered due to how AngularJS renders views with "ng-if".
2023-09-11 15:12:48 +02:00
Andreas ZerbstandGitHub 2fc29e638c Limbo models builder changes the content to element when using Compositions (#14776) 2023-09-07 17:18:57 +02:00
Andreas ZerbstandGitHub 56486b1ace V10 Cherrypicked issuu RTE embed (#14777)
* Added a UserAgent to header

* Added the use of iframe. This allows us to see the issuu embed in the RTE
2023-09-07 16:28:11 +02:00
Bjarke Berg fd043f6ecf Merge remote-tracking branch 'origin/v10/dev' into release/10.7 2023-09-05 10:19:13 +02:00
Bjarke Berg 39a102ed8a rollback version 2023-09-05 10:14:21 +02:00
f750bca453 V10/bugfix/14543 publish descendants (#14763)
* WIP: Fix publish descendants and related notifications

* Removed related entitities from publish notification

* Fixed root not being saved when publishingWithDescendants

* Updated integrationtests to reflect the update view on when to save the root when its part of a branch

* PR formatting fix

Co-authored-by: Nikolaj Geisle <70372949+Zeegaan@users.noreply.github.com>

* PR Cleanup

Co-authored-by: Nikolaj Geisle <70372949+Zeegaan@users.noreply.github.com>

* Spicing up the codebase with some PR pattern matching

Co-authored-by: Nikolaj Geisle <70372949+Zeegaan@users.noreply.github.com>

---------

Co-authored-by: Sven Geusens <sge@umbraco.dk>
Co-authored-by: Nikolaj Geisle <70372949+Zeegaan@users.noreply.github.com>
2023-09-05 10:14:06 +02:00
Bjarke Berg 7e36203f60 Merge remote-tracking branch 'origin/release/10.7' into v10/dev
# Conflicts:
#	version.json
2023-09-04 10:24:22 +02:00
Bjarke Berg 334dff8645 bump version number 2023-09-04 10:23:44 +02:00
Bjarke Berg 9fd140f81d Merge remote-tracking branch 'origin/release/10.7' into v10/dev 2023-08-28 11:35:41 +02:00
Bjarke BergandGitHub 1198c76d67 Remove parsing of short into integer (#14721) 2023-08-25 13:07:42 +02:00
Nikolaj 869b480dae Bump version 2023-08-24 10:49:50 +02:00
Nikolaj 12704b112e Merge branch 'release/10.7' into v10/dev 2023-08-24 10:49:11 +02:00
Ronald BarendseandNikolaj 3c37653012 Fix exceptions in Slider and Tags property value converters (#13782)
* Fix IndexOutOfRangeException when converting single value to range in SliderValueConverter

* Fix NullReferenceException while deserializing empty value in TagsValueConverter

* Use invariant decimal parsing

* Handle converting from slider to single value

* Fix parsing range as single value

* Make Handle methods autonomous

---------

Co-authored-by: nikolajlauridsen <nikolajlauridsen@protonmail.ch>
2023-08-23 10:50:25 +02:00
e5a8d01004 Fix exceptions in Slider and Tags property value converters (#13782)
* Fix IndexOutOfRangeException when converting single value to range in SliderValueConverter

* Fix NullReferenceException while deserializing empty value in TagsValueConverter

* Use invariant decimal parsing

* Handle converting from slider to single value

* Fix parsing range as single value

* Make Handle methods autonomous

---------

Co-authored-by: nikolajlauridsen <nikolajlauridsen@protonmail.ch>
2023-08-23 10:09:43 +02:00
Andy ButlandandBjarke Berg 9fd0b1986c Updated JSON schema reference for Umbraco Forms. (#14701) 2023-08-22 13:30:26 +02:00
Andy ButlandandGitHub 80ea5174c6 Updated JSON schema reference for Umbraco Forms. (#14701) 2023-08-22 13:28:01 +02:00
Niels Lyngsøandkjac e1b4aebb0f directly render labels without angularJS template code (#14700) 2023-08-22 11:35:38 +02:00
Niels LyngsøandGitHub 38910a8d5c directly render labels without angularJS template code (#14700) 2023-08-22 11:27:16 +02:00
Kenn JacobsenandGitHub 0dc18982ee Do not allow content type property aliases that conflict with IPublishedElement (#14683) 2023-08-21 13:17:15 +02:00
311d322129 Add code infrastructure to validate file content (#14657)
* Implemented modular architecture for filestream security sanitization with an svg-html example

* 31440: Refactoring, applied to more entry points and removed test analyzer

* 31440 Added Unittests for FileStreamSecurityValidator

* PR fixes and better unittest mock names

---------

Co-authored-by: Sven Geusens <sge@umbraco.dk>
2023-08-21 13:08:26 +02:00
Andreas ZerbstandGitHub e7af98027d Updated the name of our Acceptance test project in the pipeline (#14691)
* Updated the name of our Acceptance test project.

The reason is because we want the naming to be clear and concise.

* Changed the name of the e2e pipeline project name for linux

* Updated dll file
2023-08-18 13:36:54 +03:00
Anders ReusandBjarke Berg 30f0cfc15d Add exclusion filter setting to typefinder settings (#14426)
* Obsolete constructor with deprecated IScopeProvider

* Add exclusion setting to typefinder settings

* The old TypeFinder constructor calls the new constructor now instead and excluded duplicates by using Union instead of Concat.

* Revert "The old TypeFinder constructor calls the new constructor now instead and excluded duplicates by using Union instead of Concat."

This reverts commit 87801c6c1c.

* Add changes to TypeFinder

* Do not use null when type is not nullable

---------

Co-authored-by: Bjarke Berg <mail@bergmania.dk>
2023-08-15 09:58:58 +02:00
Ronald BarendseandBjarke Berg b743e715d4 Add DynamicRequestCultureProviderBase and improve locking (#14064) 2023-08-10 11:16:23 +02:00
d3243f8700 Bugfix 31584 - Remove Lazy "caching" that retains keys of deleted ContentTypes which causes unecesary validation errors (#14643)
The validation errors should not happen as the model mapping cleans up block data for entity types that do not longer exist (which is checked on the aformentioned key)

Co-authored-by: Sven Geusens <sge@umbraco.dk>
2023-08-09 13:11:44 +02:00
264e4f8b57 Add config so it is configurable whether to explicitly index each nested property (#14648)
* Added new configuration "Umbraco:CMS:Examine:ExplicitlyIndexEachNestedProperty" that can be used to avoid nested properties like block grid to index each property individually.

* Moved the setting "Umbraco:CMS:Examine:ExplicitlyIndexEachNestedProperty" to "Umbraco:CMS:Indexing:ExplicitlyIndexEachNestedProperty" to make it more future proof

* Added missing registration

* Fixed registration

* Small readability improvement

---------

Co-authored-by: Sven Geusens <sge@umbraco.dk>
2023-08-09 11:47:45 +02:00
Sven Geusens 2b3f048db7 Revert "Implemented modular architecture for filestream security sanitization with an svg-html example"
This reverts commit 18ee016ae9.
2023-08-08 16:26:59 +02:00
Sven Geusens 18ee016ae9 Implemented modular architecture for filestream security sanitization with an svg-html example 2023-08-08 14:49:18 +02:00
Bjarke BergandGitHub 512199c8ab Pass the existing notification state to the ContentUnpublishingNotification so it can be shared. (#14638) 2023-08-08 09:40:44 +02:00
Bjarke BergandGitHub 4e410a9dd5 Stop event propagation when clicking on the expand-arrow in the tree. (#14637) 2023-08-08 09:27:23 +02:00
Jacob OvergaardandNiels Lyngsø e16e36c68f V12: Remove avatar step from login (#14629)
Co-authored-by: Niels Lyngsø <niels.lyngso@gmail.com>
2023-08-07 11:02:53 +02:00
Bjarke Berg f384e0c30c Allow newlines in invite email and escape other html (#14618) 2023-08-02 14:17:39 +02:00
Bjarke Berg 04b74dddd0 Added missing async awaits in notifications that ensures objects are not disposed too early
Fixes https://github.com/umbraco/Umbraco-CMS/issues/14574
2023-08-02 11:37:39 +02:00
Kenn JacobsenandGitHub 43700d2bcb Ensure that the Slider does not crash the back-office (#14601)
* Ensure that the Slider does not crash the back-office

* Add field descriptions to config
2023-07-25 08:39:59 +02:00
Adrian CojocariuandGitHub eed9feb5f3 Fix method invoke. (#14597) 2023-07-24 15:18:23 +02:00
b2a5ce3e48 V10: Dropzone should handle internal and external errors when uploading (#14578)
* fix: mark files that result in error as processed

* fix: for safety measure check that a file is truthy before trying to upload it

* fix: push an error when file.$error is encountered to make sure it does not get uploaded

* fix: remove header from error messages since it is not being used anyway

* fix: check for maxFileSize before uploading pasted images in tinymce

* use stored blob variable

* feat: add property to fileManager to get and format the maxFileSize

* fix: make tinymce use fileManager to get maxFileSize

* fix(image cropper): check for maxFileSize before setting file to upload

* multiply by 1000 to get bytes

---------

Co-authored-by: Elitsa <elm@umbraco.dk>
2023-07-24 10:17:21 +02:00
1976 changed files with 52656 additions and 37663 deletions
+4
View File
@@ -22,3 +22,7 @@ RUN if [ "${NODE_VERSION}" != "none" ]; then su vscode -c "umask 0002 && . /usr/
# https://docs.npmjs.com/cli/v6/using-npm/config#unsafe-perm
# Default: false if running as root, true otherwise (we are ROOT)
#RUN npm -g config set user vscode && npm -g config set unsafe-perm
# Generate and trust a local developer certificate for Kestrel
# This is needed for Kestrel to bind on https
RUN dotnet dev-certs https --trust
+2 -52
View File
@@ -6,7 +6,6 @@
"service": "app",
"workspaceFolder": "/workspace",
// Set *default* container specific settings.json values on container create.
"settings": {
"omnisharp.defaultLaunchSolution": "umbraco.sln",
@@ -14,62 +13,13 @@
"omnisharp.enableRoslynAnalyzers": true
},
"features": {
// Workaround until the image is updated to include the latest version of .NET Core - .NET7
// https://github.com/devcontainers/templates/issues/38#issuecomment-1310803259
"ghcr.io/devcontainers/features/dotnet:1": {
"version": "7"
},
// Adds SSH support to the container
// Allowing the Github CLI `gh codespace ssh` to work
"ghcr.io/devcontainers/features/sshd:1": {
"version": "latest"
},
// Adds SQLite feature from apt install
// Also adds the SQLite VSCode extension
"ghcr.io/warrenbuckley/codespace-features/sqlite:1": {}
},
// Add the IDs of extensions you want installed when the container is created.
"extensions": [
"ms-dotnettools.csharp"
],
// This is used in the prebuilds - so dotnet build (nuget restore and node stuff) is done
"updateContentCommand": "dotnet build umbraco.sln && dotnet dev-certs https --trust",
// Use 'forwardPorts' to make a list of ports inside the container available locallAdd "forwardPorts": [9000, 5000, 25],
// Port needed to help discover SMTP4Dev
"forwardPorts": [
5000
],
"portsAttributes": {
"5000": {
"label": "SMTP4Dev",
"protocol": "http",
"onAutoForward": "notify"
},
"9000": {
"label": "Umbraco HTTP",
"protocol": "http",
"onAutoForward": "notify"
},
"44331": {
"label": "Umbraco HTTPS",
"protocol": "https",
"onAutoForward": "notify"
}
},
"customizations": {
"codespaces": {
"openFiles": [
".github/codespaces-readme.md"
]
}
}
// Use 'forwardPorts' to make a list of ports inside the container available locally.
"forwardPorts": [9000, 5000, 25]
// [Optional] To reuse of your local HTTPS dev cert:
//
-1
View File
@@ -248,7 +248,6 @@ csharp_preserve_single_line_blocks = true
##########################################
[*.{cs,csx,cake,vb,vbx}]
dotnet_diagnostic.CS1591.severity = suggestion
##########################################
# Styles
+149 -18
View File
@@ -1,4 +1,4 @@
# Umbraco CMS Build
# Umbraco CMS Build
## Are you sure?
@@ -27,7 +27,7 @@ If you want to run a build without debugging, see [Building from source](#buildi
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 v7+](https://dotnet.microsoft.com/en-us/download)
* [dotnet SDK v6.0.2+](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/)
@@ -72,7 +72,7 @@ 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)
* [Visual Studio 2019 v16.8+ with .NET 6.0.2+](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/)
@@ -99,33 +99,111 @@ When the page eventually loads in your web browser, you can follow the installer
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/`
### Quick!
You may want to build a set of NuGet packages with your changes, this can be done using the dotnet pack command.
To build Umbraco, fire up PowerShell and move to Umbraco's repository root (the directory that contains `src`, `build`, `LICENSE.md`...). There, trigger the build with the following command:
First enter the root of the project in a command line environment, and then use the following command to build the NuGet packages:
build/build.ps1
`dotnet pack -c Release -o Build.Out`
If you only see a build.bat-file, you're probably on the wrong branch. If you switch to the correct branch (v8/contrib) the file will appear and you can build it.
This will restore and build the project using the release configuration, and put all the outputted files in a folder called `Build.Out`
You might run into [Powershell quirks](#powershell-quirks).
You can then add these as a local NuGet feed using the following command:
If it runs without errors; Hooray! Now you can continue with [the next step](CONTRIBUTING.md#how-do-i-begin) and open the solution and build it.
`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.
### Build Infrastructure
The Umbraco Build infrastructure relies on a PowerShell object. The object can be retrieved with:
$ubuild = build/build.ps1 -get
The object exposes various properties and methods that can be used to fine-grain build Umbraco. Some, but not all, of them are detailed below.
#### Properties
The object exposes the following properties:
* `SolutionRoot`: the absolute path to the solution root
* `VisualStudio`: a Visual Studio object (see below)
* `NuGet`: the absolute path to the NuGet executable
* `Zip`: the absolute path to the 7Zip executable
* `VsWhere`: the absolute path to the VsWhere executable
* `NodePath`: the absolute path to the Node install
* `NpmPath`: the absolute path to the Npm install
The Visual Studio object is `null` when Visual Studio has not been detected (eg on VSTS). When not null, the object exposes the following properties:
* `Path`: Visual Studio installation path (eg some place under `Program Files`)
* `Major`: Visual Studio major version (eg `15` for VS 2017)
* `Minor`: Visual Studio minor version
* `MsBuild`: the absolute path to the MsBuild executable
#### GetUmbracoVersion
Gets an object representing the current Umbraco version. Example:
$v = $ubuild.GetUmbracoVersion()
Write-Host $v.Semver
The object exposes the following properties:
* `Semver`: the semver object representing the version
* `Release`: the main part of the version (eg `7.6.33`)
* `Comment`: the pre release part of the version (eg `alpha02`)
* `Build`: the build number part of the version (eg `1234`)
#### SetUmbracoVersion
Modifies Umbraco files with the new version.
>This entirely replaces the legacy `UmbracoVersion.txt` file. Do *not* edit version infos in files.
The version must be a valid semver version. It can include a *pre release* part (eg `alpha02`) and/or a *build number* (eg `1234`). Examples:
$ubuild.SetUmbracoVersion("7.6.33")
$ubuild.SetUmbracoVersion("7.6.33-alpha.2")
$ubuild.SetUmbracoVersion("7.6.33+1234")
$ubuild.SetUmbracoVersion("7.6.33-beta.5+5678")
#### Build
Builds Umbraco. Temporary files are generated in `build.tmp` while the actual artifacts (zip files, NuGet packages...) are produced in `build.out`. Example:
$ubuild.Build()
Some log files, such as MsBuild logs, are produced in `build.tmp` too. The `build` directory should remain clean during a build.
**Note: web.config**
Building Umbraco requires a clean `web.config` file in the `Umbraco.Web.UI` project. If a `web.config` file already exists, the `pre-build` task (see below) will save it as `web.config.temp-build` and replace it with a clean copy of `web.Template.config`. The original file is replaced once it is safe to do so, by the `pre-packages` task.
#### Build-UmbracoDocs
Builds umbraco documentation. Temporary files are generated in `build.tmp` while the actual artifacts (docs...) are produced in `build.out`. Example:
Build-UmbracoDocs
Some log files, such as MsBuild logs, are produced in `build.tmp` too. The `build` directory should remain clean during a build.
#### Verify-NuGet
Verifies that projects all require the same version of their dependencies, and that NuSpec files require versions that are consistent with projects. Example:
Verify-NuGet
### 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
At the very minimum, you want
You only have to remove the connection strings from the appsettings, but removing the data folder ensures that the sqlite database gets deleted too.
git clean -Xdf src/Umbraco.Web.UI/App_Data
rm src/Umbraco.Web.UI/web.config
Next time you run a build the `appsettings.json` file will be re-created in its default state.
Then, a simple 'Rebuild All' in Visual Studio will recreate a fresh `web.config` but should be quite fast (since it does not really need to rebuild anything).
The `clean` Git command force (`-f`) removes (`-X`, note the capital X) all files and directories (`-d`) that are ignored by Git.
This will leave media files and views around, but in most cases, it will be enough.
@@ -141,12 +219,65 @@ For git documentation see:
## Azure DevOps
Umbraco uses Azure DevOps for continuous integration, nightly builds and release builds. The Umbraco CMS project on DevOps [is available for anonymous users](https://umbraco.visualstudio.com/Umbraco%20Cms)..
Umbraco uses Azure DevOps for continuous integration, nightly builds and release builds. The Umbraco CMS project on DevOps [is available for anonymous users](https://umbraco.visualstudio.com/Umbraco%20Cms).
The produced artifacts are published in a container that can be downloaded from DevOps called "nupkg" which contains all the NuGet packages that got built.
DevOps uses the `Build-Umbraco` command several times, each time passing a different *target* parameter. The supported targets are:
* `pre-build`: prepares the build
* `compile-belle`: compiles Belle
* `compile-umbraco`: compiles Umbraco
* `pre-tests`: prepares the tests
* `compile-tests`: compiles the tests
* `pre-packages`: prepares the packages
* `pkg-zip`: creates the zip files
* `pre-nuget`: prepares NuGet packages
* `pkg-nuget`: creates NuGet packages
All these targets are executed when `Build-Umbraco` is invoked without a parameter (or with the `all` parameter). On VSTS, compilations (of Umbraco and tests) are performed by dedicated DevOps tasks. Similarly, creating the NuGet packages is also performed by dedicated DevOps tasks.
Finally, the produced artifacts are published in two containers that can be downloaded from DevOps: `zips` contains the zip files while `nuget` contains the NuGet packages.
>During a DevOps build, some environment `UMBRACO_*` variables are exported by the `pre-build` target and can be reused in other targets *and* in DevOps tasks. The `UMBRACO_TMP` environment variable is used in `Umbraco.Tests` to disable some tests that have issues with DevOps at the moment.
## Quirks
### PowerShell Quirks
There is a good chance that running `build.ps1` ends up in error, with messages such as
>The file ...\build.ps1 is not digitally signed. You cannot run this script on the current system. For more information about running scripts and setting execution policy, see about_Execution_Policies.
PowerShell has *Execution Policies* that may prevent the script from running. You can check the current policies with:
PS> Get-ExecutionPolicy -List
Scope ExecutionPolicy
----- ---------------
MachinePolicy Undefined
UserPolicy Undefined
Process Undefined
CurrentUser Undefined
LocalMachine RemoteSigned
Policies can be `Restricted`, `AllSigned`, `RemoteSigned`, `Unrestricted` and `Bypass`. Scopes can be `MachinePolicy`, `UserPolicy`, `Process`, `CurrentUser`, `LocalMachine`. You need the current policy to be `RemoteSigned`&mdash;as long as it is `Undefined`, the script cannot run. You can change the current user policy with:
PS> Set-ExecutionPolicy -Scope CurrentUser -ExecutionPolicy RemoteSigned
Alternatively, you can do it at machine level, from within an elevated PowerShell session:
PS> Set-ExecutionPolicy -Scope LocalMachine -ExecutionPolicy RemoteSigned
And *then* the script should run. It *might* however still complain about executing scripts, with messages such as:
>Security warning - Run only scripts that you trust. While scripts from the internet can be useful, this script can potentially harm your computer. If you trust this script, use the Unblock-File cmdlet to allow the script to run without this warning message. Do you want to run ...\build.ps1?
[D] Do not run [R] Run once [S] Suspend [?] Help (default is "D"):
This is usually caused by the scripts being *blocked*. And that usually happens when the source code has been downloaded as a Zip file. When Windows downloads Zip files, they are marked as *blocked* (technically, they have a Zone.Identifier alternate data stream, with a value of "3" to indicate that they were downloaded from the Internet). And when such a Zip file is un-zipped, each and every single file is also marked as blocked.
The best solution is to unblock the Zip file before un-zipping: right-click the files, open *Properties*, and there should be a *Unblock* checkbox at the bottom of the dialog. If, however, the Zip file has already been un-zipped, it is possible to recursively unblock all files from PowerShell with:
PS> Get-ChildItem -Recurse *.* | Unblock-File
### Git Quirks
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).
+15 -19
View File
@@ -103,7 +103,7 @@ Great question! The short version goes like this:
1. **Switch to the correct branch**
Switch to the `contrib` branch
Switch to the `v10/contrib` branch
1. **Build**
@@ -111,7 +111,7 @@ Great question! The short version goes like this:
1. **Branch**
Create a new branch now and name it after the issue you're fixing, we usually follow the format: `temp-12345`. This means it's a temporary branch for the particular issue you're working on, in this case issue number `12345`. Don't commit to `contrib`, create a new branch first.
Create a new branch now and name it after the issue you're fixing, we usually follow the format: `temp-12345`. This means it's a temporary branch for the particular issue you're working on, in this case issue number `12345`. Don't commit to `v10/contrib`, create a new branch first.
1. **Change**
@@ -121,7 +121,7 @@ Great question! The short version goes like this:
Done? Yay! 🎉
Remember to commit to your new `temp` branch, and don't commit to `contrib`. Then you can push the changes up to your fork on GitHub.
Remember to commit to your new `temp` branch, and don't commit to `v10/contrib`. Then you can push the changes up to your fork on GitHub.
#### Keeping your Umbraco fork in sync with the main repository
[sync fork]: #keeping-your-umbraco-fork-in-sync-with-the-main-repository
@@ -138,10 +138,10 @@ Then when you want to get the changes from the main repository:
```
git fetch upstream
git rebase upstream/contrib
git rebase upstream/v10/contrib
```
In this command we're syncing with the `contrib` branch, but you can of course choose another one if needed.
In this command we're syncing with the `v10/contrib` branch, but you can of course choose another one if needed.
[More information on how this works can be found on the thoughtbot blog.][sync fork ext]
@@ -169,7 +169,7 @@ We recommend you to [sync with our repository][sync fork] before you submit your
GitHub will have picked up on the new branch you've pushed and will offer to create a Pull Request. Click that green button and away you go.
![Create a pull request](img/createpullrequest.png)
We like to use [git flow][git flow] as much as possible, but don't worry if you are not familiar with it. The most important thing you need to know is that when you fork the Umbraco repository, the default branch is set to `contrib`. This is the branch you should be targeting.
We like to use [git flow][git flow] as much as possible, but don't worry if you are not familiar with it. The most important thing you need to know is that when you fork the Umbraco repository, the default branch is set to something, usually `v10/contrib`. If you are working on v9, this is the branch you should be targeting.
Please note: we are no longer accepting features for v8 and below but will continue to merge security fixes as and when they arise.
@@ -208,13 +208,11 @@ There will be times that we really like your proposed changes and well finish
The Core Contributors team consists of one member of Umbraco HQ, [Sebastiaan][Sebastiaan], who gets assistance from the following community members who have committed to volunteering their free time:
- [Busra Sengul][Busra Sengul]
- [Emma Garland][Emma Garland]
- [George Bidder][George Bidder]
- [Jason Elkin][Jason Elkin]
- [Laura Neto][Laura Neto]
- [Nathan Woulfe][Nathan Woulfe]
- [Joe Glombek][Joe Glombek]
- [Laura Weatherhead][Laura Weatherhead]
- [Michael Latouche][Michael Latouche]
- [Sebastiaan][Sebastiaan]
- [Owain Williams][Owain Williams]
These wonderful people aim to provide you with a reply to your PR, review and test out your changes and on occasions, they might ask more questions. If they are happy with your work, they'll let Umbraco HQ know by approving the PR. HQ will have final sign-off and will check the work again before it is merged.
@@ -229,14 +227,12 @@ These wonderful people aim to provide you with a reply to your PR, review and te
<!-- External -->
[Busra Sengul]: https://github.com/busrasengul "Busra's GitHub profile"
[Emma Garland]: https://github.com/emmagarland "Emma's GitHub profile"
[George Bidder]: https://github.com/georgebid "George's GitHub profile"
[Jason Elkin]: https://github.com/jasonelkin "Jason's GitHub profile"
[Kyle Eck]: https://github.com/teckspeed "Kyle's GitHub profile"
[Laura Neto]: https://github.com/lauraneto "Laura's GitHub profile"
[Nathan Woulfe]: https://github.com/nathanwoulfe "Nathan's GitHub profile"
[Joe Glombek]: https://github.com/glombek "Joe's GitHub profile"
[Laura Weatherhead]: https://github.com/lssweatherhead "Laura's GitHub profile"
[Michael Latouche]: https://github.com/mikecp "Michael's GitHub profile"
[Sebastiaan]: https://github.com/nul800sebastiaan "Sebastiaan's GitHub profile"
[Owain Williams]: https://github.com/OwainWilliams "Owain's GitHub profile"
[Sebastiaan]: https://github.com/nul800sebastiaan "Senastiaan's GitHub profile"
[ Umbrabot ]: https://github.com/umbrabot
[git flow]: https://jeffkreeftmeijer.com/git-flow/ "An explanation of git flow"
[sync fork ext]: http://robots.thoughtbot.com/post/5133345960/keeping-a-git-fork-updated "Details on keeping a git fork updated"
+2 -10
View File
@@ -1,11 +1,4 @@
# [Umbraco CMS](https://umbraco.com)
[![GitHub license](https://img.shields.io/badge/license-MIT-blue.svg)](../LICENSE.md)
[![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](CONTRIBUTING.md)
[![Follow Umbraco on Twitter](https://img.shields.io/badge/Follow-blue?logo=twitter&logoColor=fff)](https://twitter.com/intent/follow?screen_name=umbraco)
[![Chat about Umbraco on Discord](https://img.shields.io/discord/869656431308189746?logo=discord&logoColor=fff)](https://discord.gg/umbraco)
[![Build status](https://img.shields.io/azure-devops/build/umbraco/Umbraco%2520Cms/301?logo=azurepipelines&label=Azure%20Pipelines)](https://umbraco.visualstudio.com/Umbraco%20Cms/_build?definitionId=301)
[![Open in GitHub Codespaces](https://img.shields.io/badge/Open%20in%20GitHub%20Codespaces-525252?logo=github)](https://github.com/codespaces/new?hide_repo_select=true&ref=contrib&repo=10601208&machine=basicLinux32gb&devcontainer_path=.devcontainer%2Fdevcontainer.json&location=WestEurope)
# [Umbraco CMS](https://umbraco.com) &middot; [![GitHub license](https://img.shields.io/badge/license-MIT-blue.svg)](../LICENSE.md) [![Build status](https://umbraco.visualstudio.com/Umbraco%20Cms/_apis/build/status/Cms%208%20Continuous?branchName=v8/contrib)](https://umbraco.visualstudio.com/Umbraco%20Cms/_build?definitionId=75) [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](CONTRIBUTING.md) [![Twitter](https://img.shields.io/twitter/follow/umbraco.svg?style=social&label=Follow)](https://twitter.com/intent/follow?screen_name=umbraco) [![Discord](https://img.shields.io/discord/869656431308189746)](https://discord.gg/umbraco)
Umbraco is the friendliest, most flexible and fastest growing ASP.NET CMS, and used by more than 500,000 websites worldwide. Our mission is to help you deliver delightful digital experiences by making Umbraco friendly, simpler and social.
@@ -38,8 +31,7 @@ The documentation for Umbraco CMS can be found [on Our Umbraco](https://docs.umb
Our friendly community is available 24/7 at the community hub, we call ["Our Umbraco"](https://our.umbraco.com/). Our Umbraco features forums for questions and answers, documentation, downloadable plugins for Umbraco, and a rich collection of community resources.
Besides "Our", we all support each other in our [Community Discord Server](https://discord.gg/umbraco) and on Twitter: [Umbraco HQ](https://twitter.com/umbraco), [Release Updates](https://twitter.com/umbracoproject), [#umbraco](https://twitter.com/hashtag/umbraco)
Besides "Our", we all support each other also via Twitter: [Umbraco HQ](https://twitter.com/umbraco), [Release Updates](https://twitter.com/umbracoproject), [#umbraco](https://twitter.com/hashtag/umbraco)
## Contributing
-21
View File
@@ -1,21 +0,0 @@
# GitHub CodeSpaces
Umbraco source code can be edited inside the browser with VSCode and CodeSpaces.
This development environment comes with all the tools you need to build Umbraco source code.
## Debugging and Running
From VSCode browse to the Run and Debug section and then click the green button. This will build the Umbraco source code and attach a debugger and launch the site.
## Default Umbraco credentials
Username: test@umbraco.com
Password: password1234
## Test Email Server
A SMTP4Dev instance for testing email is available on port 5000.
## SQLite Database
The SQLite extension is preinstalled and allows you to open, query, edit the data inside the Umbraco SQLite database for ease of use.
@@ -0,0 +1,58 @@
name: Add issues to review project
on:
issues:
types:
- opened
permissions:
contents: read
jobs:
get-user-type:
runs-on: ubuntu-latest
outputs:
ignored: ${{ steps.set-output.outputs.ignored }}
steps:
- name: Install dependencies
run: |
npm install node-fetch@2
- uses: actions/github-script@v5
name: "Determing HQ user or not"
id: set-output
with:
script: |
const fetch = require('node-fetch');
const response = await fetch('https://collaboratorsv2.euwest01.umbraco.io/umbraco/api/users/IsIgnoredUser', {
method: 'post',
body: JSON.stringify('${{ github.event.issue.user.login }}'),
headers: {
'Authorization': 'Bearer ${{ secrets.OUR_BOT_API_TOKEN }}',
'Content-Type': 'application/json'
}
});
var isIgnoredUser = true;
try {
if(response.status === 200) {
const data = await response.text();
isIgnoredUser = data === "true";
} else {
console.log("Returned data not indicate success:", response.status);
}
} catch(error) {
console.log(error);
};
core.setOutput("ignored", isIgnoredUser);
console.log("Ignored is", isIgnoredUser);
add-to-project:
permissions:
repository-projects: write # for actions/add-to-project
if: needs.get-user-type.outputs.ignored == 'false'
runs-on: ubuntu-latest
needs: [get-user-type]
steps:
- uses: actions/add-to-project@main
with:
project-url: https://github.com/orgs/${{ github.repository_owner }}/projects/21
github-token: ${{ secrets.ADD_TO_PROJECT_PAT }}
+2 -2
View File
@@ -15,8 +15,8 @@ permissions:
contents: read
env:
dotnetVersion: 7.x
dotnetIncludePreviewVersions: true
dotnetVersion: 6.x
dotnetIncludePreviewVersions: false
solution: umbraco.sln
buildConfiguration: SkipTests
DOTNET_NOLOGO: true
@@ -18,6 +18,8 @@ jobs:
uses: actions/github-script@v6
with:
script: |
const fetch = require('node-fetch')
const response = await fetch('https://collaboratorsv2.euwest01.umbraco.io/umbraco/api/comments/PostComment', {
method: 'post',
body: JSON.stringify({
@@ -20,6 +20,7 @@ jobs:
uses: actions/github-script@v6
with:
script: |
const fetch = require('node-fetch');
const response = await fetch('https://collaboratorsv2.euwest01.umbraco.io/umbraco/api/comments/PostComment', {
method: 'post',
body: JSON.stringify({
+2 -8
View File
@@ -101,12 +101,6 @@ preserve.belle
/tests/Umbraco.Tests.UnitTests/[Uu]mbraco/[Dd]ata/TEMP/
# Ignore auto-generated schema
/src/Umbraco.Cms.Targets/tasks/
/src/Umbraco.Cms.Targets/appsettings-schema.*.json
/src/Umbraco.Web.UI/appsettings-schema.json
/src/Umbraco.Web.UI/appsettings-schema.*.json
/tests/Umbraco.Tests.Integration/appsettings-schema.json
/tests/Umbraco.Tests.Integration/appsettings-schema.*.json
/src/Umbraco.Cms/appsettings-schema.json
playwright-report
trace.zip
/src/Umbraco.Web.UI/appsettings-schema.json
/tests/Umbraco.Tests.Integration/appsettings-schema.json
+1
View File
@@ -8,6 +8,7 @@
"name": ".NET Core Launch (web)",
"type": "coreclr",
"request": "launch",
"preLaunchTask": "Dotnet build",
"program": "dotnet",
"args": ["run"],
"cwd": "${workspaceFolder}/src/Umbraco.Web.UI",
+4 -5
View File
@@ -1,8 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<Project>
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<LangVersion>latest</LangVersion>
<TargetFramework>net6.0</TargetFramework>
<Company>Umbraco HQ</Company>
<Authors>Umbraco</Authors>
<Copyright>Copyright © Umbraco $([System.DateTime]::Today.ToString('yyyy'))</Copyright>
@@ -29,9 +28,8 @@
<!-- Package Validation -->
<PropertyGroup>
<GenerateCompatibilitySuppressionFile>false</GenerateCompatibilitySuppressionFile>
<EnablePackageValidation>true</EnablePackageValidation>
<PackageValidationBaselineVersion>12.0.0</PackageValidationBaselineVersion>
<PackageValidationBaselineVersion>10.0.0</PackageValidationBaselineVersion>
<EnableStrictModeForCompatibleFrameworksInPackage>true</EnableStrictModeForCompatibleFrameworksInPackage>
<EnableStrictModeForCompatibleTfms>true</EnableStrictModeForCompatibleTfms>
</PropertyGroup>
@@ -41,7 +39,7 @@
<PackageReference Include="Nerdbank.GitVersioning" Version="3.5.113" PrivateAssets="all" IsImplicitlyDefined="true" />
<PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.406" PrivateAssets="all" IsImplicitlyDefined="true" />
<PackageReference Include="Umbraco.Code" Version="2.0.0" PrivateAssets="all" IsImplicitlyDefined="true" />
<PackageReference Include="Umbraco.GitVersioning.Extensions" Version="0.2.0" PrivateAssets="all" IsImplicitlyDefined="true" />
<PackageReference Include="Umbraco.GitVersioning.Extensions" Version="0.1.1" PrivateAssets="all" IsImplicitlyDefined="true" />
</ItemGroup>
<ItemGroup>
@@ -51,4 +49,5 @@
<PropertyGroup>
<GitVersionBaseDirectory>$(MSBuildThisFileDirectory)</GitVersionBaseDirectory>
</PropertyGroup>
</Project>
+101 -41
View File
@@ -21,11 +21,35 @@ parameters:
displayName: Upload API docs
type: boolean
default: false
- name: forceReleaseTestFilter
displayName: Force to use the release test filters
type: boolean
default: false
- name: integrationNonReleaseTestFilter
displayName: TestFilter used for non-release type builds
type: string
default: '--filter TestCategory!=LongRunning&TestCategory!=NonCritical'
- name: integrationReleaseTestFilter
displayName: TestFilter used for release type builds
type: string
default: ' '
- name: nonWindowsIntegrationNonReleaseTestFilter
displayName: TestFilter used for non-release type builds on non windows agents
type: string
default: '--filter TestCategory!=LongRunning&TestCategory!=NonCritical'
- name: nonWindowsIntegrationReleaseTestFilter
displayName: TestFilter used for release type builds on non windows agents
type: string
default: ' '
- name: isNightly
displayName: 'Is nightly build (used for MyGet feed)'
type: boolean
default: false
variables:
nodeVersion: 18.16.0
dotnetVersion: 7.x
dotnetIncludePreviewVersions: true
nodeVersion: 20
dotnetVersion: 6.x
dotnetIncludePreviewVersions: false
solution: umbraco.sln
buildConfiguration: Release
UMBRACO__CMS__GLOBAL__ID: 00000000-0000-0000-0000-000000000042
@@ -49,6 +73,7 @@ stages:
steps:
- task: NodeTool@0
displayName: Use Node.js $(nodeVersion)
retryCountOnTaskFailure: 3
inputs:
versionSpec: $(nodeVersion)
- task: Cache@2
@@ -61,9 +86,9 @@ stages:
path: $(npm_config_cache)
- script: npm ci --no-fund --no-audit --prefer-offline
workingDirectory: src/Umbraco.Web.UI.Client
displayName: Run npm ci
displayName: Run npm ci (Backoffice)
- task: gulp@0
displayName: Run gulp build
displayName: Run gulp build (Backoffice)
inputs:
gulpFile: src/Umbraco.Web.UI.Client/gulpfile.js
targets: coreBuild
@@ -72,6 +97,7 @@ stages:
displayName: Use .NET $(dotnetVersion)
inputs:
version: $(dotnetVersion)
performMultiLevelLookup: true
includePreviewVersions: $(dotnetIncludePreviewVersions)
- task: DotNetCoreCLI@2
displayName: Run dotnet restore
@@ -79,29 +105,12 @@ stages:
command: restore
projects: $(solution)
- task: DotNetCoreCLI@2
displayName: Run dotnet build
name: build
displayName: Run dotnet build and generate NuGet packages
inputs:
command: build
projects: $(solution)
arguments: '--configuration $(buildConfiguration) --no-restore -p:ContinuousIntegrationBuild=true'
- script: |
version="$(Build.BuildNumber)"
echo "Version: $version"
major="$(echo $version | cut -d '.' -f 1)"
echo "Major version: $major"
echo "##vso[task.setvariable variable=majorVersion;isOutput=true]$major"
displayName: Set major version
name: determineMajorVersion
- script: dotnet pack $(solution) --configuration $(buildConfiguration) --no-build --property:PackageOutputPath=$(Build.ArtifactStagingDirectory)/nupkg
displayName: Run dotnet pack
- script: |
sha="$(Build.SourceVersion)"
sha=${sha:0:7}
buildnumber="$(Build.BuildNumber)_$(Build.BuildId)_$sha"
echo "##vso[build.updatebuildnumber]$buildnumber"
displayName: Update build number
arguments: '--configuration $(buildConfiguration) --no-restore --property:ContinuousIntegrationBuild=true --property:GeneratePackageOnBuild=true --property:PackageOutputPath=$(Build.ArtifactStagingDirectory)/nupkg'
- task: PublishPipelineArtifact@1
displayName: Publish nupkg
inputs:
@@ -114,11 +123,11 @@ stages:
artifactName: build_output
- stage: Build_Docs
condition: and(succeeded(), or(startsWith(variables['Build.SourceBranch'], 'refs/heads/release/'), ${{parameters.buildApiDocs}}))
condition: and(succeeded(), or(eq(dependencies.Build.outputs['A.build.NBGV_PublicRelease'], 'True'), ${{parameters.buildApiDocs}}))
displayName: Prepare API Documentation
dependsOn: Build
variables:
umbracoMajorVersion: $[ stageDependencies.Build.A.outputs['determineMajorVersion.majorVersion'] ]
umbracoMajorVersion: $[ stageDependencies.Build.A.outputs['build.NBGV_VersionMajor'] ]
jobs:
# C# API Reference
- job:
@@ -172,9 +181,10 @@ stages:
vmImage: 'ubuntu-latest'
steps:
- task: NodeTool@0
displayName: Use Node.js 10.15.0
displayName: Use Node.js 10.15.x
retryCountOnTaskFailure: 3
inputs:
versionSpec: 10.15.0 # Won't work with higher versions
versionSpec: 10.15.x # Won't work with higher versions
- script: |
npm ci --no-fund --no-audit --prefer-offline
npx gulp docs
@@ -230,6 +240,7 @@ stages:
displayName: Use .NET $(dotnetVersion)
inputs:
version: $(dotnetVersion)
performMultiLevelLookup: true
includePreviewVersions: $(dotnetIncludePreviewVersions)
- task: DotNetCoreCLI@2
displayName: Run dotnet test
@@ -242,6 +253,8 @@ stages:
- stage: Integration
displayName: Integration Tests
dependsOn: Build
variables:
releaseTestFilter: eq(dependencies.Build.outputs['A.build.NBGV_PublicRelease'], 'True')
jobs:
# Integration Tests (SQLite)
- job:
@@ -266,14 +279,33 @@ stages:
displayName: Use .NET $(dotnetVersion)
inputs:
version: $(dotnetVersion)
performMultiLevelLookup: true
includePreviewVersions: $(dotnetIncludePreviewVersions)
- task: DotNetCoreCLI@2
displayName: Run dotnet test
displayName: Run dotnet test Windows
condition: eq(variables['Agent.OS'],'Windows_NT')
inputs:
command: test
projects: '**/*.Tests.Integration.csproj'
arguments: '--configuration $(buildConfiguration) --no-build'
testRunTitle: Integration Tests SQLite - $(Agent.OS)
${{ if or(variables.releaseTestFilter, parameters.forceReleaseTestFilter) }}:
arguments: '--configuration $(buildConfiguration) --no-build ${{parameters.integrationReleaseTestFilter}}'
${{ else }}:
arguments: '--configuration $(buildConfiguration) ${{parameters.integrationNonReleaseTestFilter}}'
env:
Tests__Database__DatabaseType: 'Sqlite'
Umbraco__CMS__Global__MainDomLock: 'FileSystemMainDomLock'
- task: DotNetCoreCLI@2
displayName: Run dotnet test Non Windows
condition: ne(variables['Agent.OS'],'Windows_NT')
inputs:
command: test
projects: '**/*.Tests.Integration.csproj'
testRunTitle: Integration Tests SQLite - $(Agent.OS)
${{ if or(variables.releaseTestFilter, parameters.forceReleaseTestFilter) }}:
arguments: '--configuration $(buildConfiguration) --no-build ${{parameters.nonWindowsIntegrationReleaseTestFilter}}'
${{ else }}:
arguments: '--configuration $(buildConfiguration) ${{parameters.nonWindowsIntegrationNonReleaseTestFilter}}'
env:
Tests__Database__DatabaseType: 'Sqlite'
Umbraco__CMS__Global__MainDomLock: 'FileSystemMainDomLock'
@@ -281,7 +313,7 @@ stages:
# Integration Tests (SQL Server)
- job:
timeoutInMinutes: 120
condition: or(startsWith(variables['Build.SourceBranch'], 'refs/heads/release/'), ${{parameters.sqlServerIntegrationTests}})
condition: or(eq(stageDependencies.Build.A.outputs['build.NBGV_PublicRelease'], 'True'), ${{parameters.sqlServerIntegrationTests}})
displayName: Integration Tests (SQL Server)
strategy:
matrix:
@@ -315,12 +347,31 @@ stages:
displayName: Start SQL Server (Linux only)
condition: and(succeeded(), eq(variables['Agent.OS'], 'Linux'))
- task: DotNetCoreCLI@2
displayName: Run dotnet test
displayName: Run dotnet test Windows
condition: eq(variables['Agent.OS'],'Windows_NT')
inputs:
command: test
projects: '**/*.Tests.Integration.csproj'
arguments: '--configuration $(buildConfiguration) --no-build'
testRunTitle: Integration Tests SQL Server - $(Agent.OS)
${{ if or(variables.releaseTestFilter, parameters.forceReleaseTestFilter) }}:
arguments: '--configuration $(buildConfiguration) --no-build ${{parameters.integrationReleaseTestFilter}}'
${{ else }}:
arguments: '--configuration $(buildConfiguration) --no-build ${{parameters.integrationNonReleaseTestFilter}}'
env:
Tests__Database__DatabaseType: $(testDb)
Tests__Database__SQLServerMasterConnectionString: $(connectionString)
Umbraco__CMS__Global__MainDomLock: 'SqlMainDomLock'
- task: DotNetCoreCLI@2
displayName: Run dotnet test NonWindows
condition: ne(variables['Agent.OS'],'Windows_NT')
inputs:
command: test
projects: '**/*.Tests.Integration.csproj'
testRunTitle: Integration Tests SQL Server - $(Agent.OS)
${{ if or(variables.releaseTestFilter, parameters.forceReleaseTestFilter) }}:
arguments: '--configuration $(buildConfiguration) --no-build ${{parameters.nonWindowsIntegrationReleaseTestFilter}}'
${{ else }}:
arguments: '--configuration $(buildConfiguration) --no-build ${{parameters.nonWindowsIntegrationNonReleaseTestFilter}}'
env:
Tests__Database__DatabaseType: $(testDb)
Tests__Database__SQLServerMasterConnectionString: $(connectionString)
@@ -356,7 +407,7 @@ stages:
Umbraco__CMS__Unattended__InstallUnattended: true
Umbraco__CMS__Global__InstallMissingDatabase: true
UmbracoDatabaseServer: (LocalDB)\MSSQLLocalDB
UmbracoDatabaseName: Playwright
UmbracoDatabaseName: AcceptanceTestDB
ConnectionStrings__umbracoDbDSN: Server=$(UmbracoDatabaseServer);Database=$(UmbracoDatabaseName);Integrated Security=true;
# Custom Umbraco settings
Umbraco__CMS__Global__VersionCheckPeriod: 0
@@ -373,6 +424,7 @@ stages:
path: $(Build.SourcesDirectory)/tests/Umbraco.Tests.AcceptanceTest/misc/nupkg
- task: NodeTool@0
displayName: Use Node.js $(nodeVersion)
retryCountOnTaskFailure: 3
inputs:
versionSpec: $(nodeVersion)
- task: Cache@2
@@ -402,6 +454,7 @@ stages:
displayName: Use .NET $(dotnetVersion)
inputs:
version: $(dotnetVersion)
performMultiLevelLookup: true
includePreviewVersions: $(dotnetIncludePreviewVersions)
- pwsh: |
$sha = 'g$(Build.SourceVersion)'.substring(0, 8)
@@ -416,7 +469,7 @@ stages:
workingDirectory: tests/Umbraco.Tests.AcceptanceTest/misc
- pwsh: |
dotnet new --install ./nupkg/Umbraco.Templates.*.nupkg
dotnet new umbraco --name Playwright --no-restore --output .
dotnet new umbraco --name AcceptanceTestProject --no-restore --output .
dotnet restore --configfile ./nuget.config
dotnet build --configuration $(buildConfiguration) --no-restore
dotnet dev-certs https
@@ -477,9 +530,11 @@ stages:
- Unit
- Integration
# - E2E # TODO: Enable when stable.
condition: and(succeeded(), or(startsWith(variables['Build.SourceBranch'], 'refs/heads/release/'), ${{parameters.myGetDeploy}}))
condition: and(succeeded(), or(eq(dependencies.Build.outputs['A.build.NBGV_PublicRelease'], 'True'), ${{parameters.myGetDeploy}}))
jobs:
- job:
pool:
vmImage: "windows-latest" # NuGetCommand@2 is no longer supported on Ubuntu 24.04 so we'll use windows until an alternative is available.
displayName: Push to pre-release feed
steps:
- checkout: none
@@ -494,15 +549,20 @@ stages:
command: 'push'
packagesToPush: $(Build.ArtifactStagingDirectory)/**/*.nupkg
nuGetFeedType: 'external'
publishFeedCredentials: 'MyGet - Pre-releases'
${{ if eq(parameters.isNightly, true) }}:
publishFeedCredentials: 'MyGet - Umbraco Nightly'
${{ else }}:
publishFeedCredentials: 'MyGet - Pre-releases'
- stage: Deploy_NuGet
displayName: NuGet release
dependsOn:
- Deploy_MyGet
- Build_Docs
condition: and(succeeded(), or(startsWith(variables['Build.SourceBranch'], 'refs/heads/release/'), ${{parameters.nuGetDeploy}}))
condition: and(succeeded(), or(eq(dependencies.Build.outputs['A.build.NBGV_PublicRelease'], 'True'), ${{parameters.nuGetDeploy}}))
jobs:
- job:
pool:
vmImage: "windows-latest" # NuGetCommand@2 is no longer supported on Ubuntu 24.04 so we'll use windows until an alternative is available.
displayName: Push to NuGet
steps:
- checkout: none
@@ -523,12 +583,12 @@ stages:
pool:
vmImage: 'windows-latest' # Apparently AzureFileCopy is windows only :(
variables:
umbracoMajorVersion: $[ stageDependencies.Build.A.outputs['determineMajorVersion.majorVersion'] ]
umbracoMajorVersion: $[ stageDependencies.Build.A.outputs['build.NBGV_VersionMajor'] ]
displayName: Upload API Documention
dependsOn:
- Build
- Deploy_NuGet
condition: and(succeeded(), or(startsWith(variables['Build.SourceBranch'], 'refs/heads/release/'), ${{parameters.uploadApiDocs}}))
condition: and(succeeded(), or(eq(dependencies.Build.outputs['A.build.NBGV_PublicRelease'], 'True'), ${{parameters.uploadApiDocs}}))
jobs:
- job:
displayName: Upload C# Docs
+35
View File
@@ -0,0 +1,35 @@
name: Nightly_$(TeamProject)_$(Build.DefinitionName)_$(SourceBranchName)_$(Date:yyyyMMdd)$(Rev:.r)
pr: none
trigger: none
schedules:
- cron: '0 0 * * *'
displayName: Daily midnight build
branches:
include:
- v10/dev
- v12/dev
- v13/dev
- v14/dev
steps:
- checkout: none
- task: TriggerBuild@4
inputs:
definitionIsInCurrentTeamProject: true
buildDefinition: '301'
queueBuildForUserThatTriggeredBuild: true
ignoreSslCertificateErrors: false
useSameSourceVersion: false
useCustomSourceVersion: false
useSameBranch: true
waitForQueuedBuildsToFinish: false
storeInEnvironmentVariable: false
templateParameters: 'sqlServerIntegrationTests: true, forceReleaseTestFilter: true, myGetDeploy: true, isNightly: true'
authenticationMethod: 'OAuth Token'
enableBuildInQueueCondition: false
dependentOnSuccessfulBuildCondition: false
dependentOnFailedBuildCondition: false
checkbuildsoncurrentbranch: false
failTaskIfConditionsAreNotFulfilled: false
+6
View File
@@ -0,0 +1,6 @@
{
"sdk": {
"version": "6.0.300",
"rollForward": "latestFeature"
}
}
+53
View File
@@ -0,0 +1,53 @@
name: issue-first-response
on:
issues:
types: [opened]
jobs:
send-response:
runs-on: ubuntu-latest
permissions:
issues: write
pull-requests: write
steps:
- name: Install dependencies
run: |
npm install node-fetch@2
- name: Fetch random comment 🗣️ and add it to the issue
uses: actions/github-script@v6
with:
script: |
const fetch = require('node-fetch')
const response = await fetch('https://collaboratorsv2.euwest01.umbraco.io/umbraco/api/comments/PostComment', {
method: 'post',
body: JSON.stringify({
repo: '${{ github.repository }}',
number: '${{ github.event.number }}',
actor: '${{ github.actor }}',
commentType: 'opened-issue-first-comment'
}),
headers: {
'Authorization': 'Bearer ${{ secrets.OUR_BOT_API_TOKEN }}',
'Content-Type': 'application/json'
}
});
try {
const data = await response.text();
if(response.status === 200 && data !== '') {
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: data
});
} else {
console.log("Status code did not indicate success:", response.status);
console.log("Returned data:", data);
}
} catch(error) {
console.log(error);
}
+67
View File
@@ -0,0 +1,67 @@
<?xml version="1.0" encoding="utf-8"?>
<Project>
<PropertyGroup>
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
<NoWarn>NU1507</NoWarn>
</PropertyGroup>
<ItemGroup>
<PackageVersion Include="Dazinator.Extensions.FileProviders" Version="2.0.0" />
<PackageVersion Include="Examine" Version="3.0.1" />
<PackageVersion Include="Examine.Core" Version="3.0.1" />
<PackageVersion Include="HtmlAgilityPack" Version="1.11.54" />
<PackageVersion Include="IPNetwork2" Version="2.6.618" />
<PackageVersion Include="K4os.Compression.LZ4" Version="1.3.6" />
<PackageVersion Include="MailKit" Version="3.2.0" />
<PackageVersion Include="Markdown" Version="2.2.1" />
<PackageVersion Include="MessagePack" Version="2.5.187" />
<PackageVersion Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="6.0.24" />
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation" Version="6.0.24" />
<PackageVersion Include="Microsoft.Data.Sqlite" Version="6.0.24" />
<PackageVersion Include="Microsoft.CSharp" Version="4.7.0" />
<PackageVersion Include="Microsoft.Extensions.FileProviders.Embedded" Version="6.0.24" />
<PackageVersion Include="Microsoft.Extensions.FileProviders.Physical" Version="6.0.0" />
<PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="6.0.0" />
<PackageVersion Include="Microsoft.Extensions.Identity.Core" Version="6.0.24" />
<PackageVersion Include="Microsoft.Extensions.Logging" Version="6.0.0" />
<PackageVersion Include="Microsoft.Extensions.Options" Version="6.0.0" />
<PackageVersion Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="6.0.0" />
<PackageVersion Include="Microsoft.Extensions.Options.DataAnnotations" Version="6.0.0" />
<PackageVersion Include="Microsoft.Extensions.Configuration.Abstractions" Version="6.0.0" />
<PackageVersion Include="Microsoft.Extensions.Configuration.Json" Version="6.0.0" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="6.0.1" />
<PackageVersion Include="Microsoft.Extensions.Http" Version="6.0.0" />
<PackageVersion Include="Microsoft.Extensions.Identity.Stores" Version="6.0.24" />
<PackageVersion Include="MiniProfiler.AspNetCore.Mvc" Version="4.2.22" />
<PackageVersion Include="MiniProfiler.Shared" Version="4.2.22" />
<PackageVersion Include="ncrontab" Version="3.3.3" />
<PackageVersion Include="Newtonsoft.Json" Version="13.0.3" />
<PackageVersion Include="NPoco.SqlServer" Version="5.3.2" />
<PackageVersion Include="Serilog" Version="2.12.0" />
<PackageVersion Include="Serilog.AspNetCore" 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="3.4.1" />
<PackageVersion Include="Serilog.Extensions.Hosting" Version="4.2.0" />
<PackageVersion Include="Serilog.Formatting.Compact" Version="1.1.0" />
<PackageVersion Include="Serilog.Formatting.Compact.Reader" Version="1.0.5" />
<PackageVersion Include="Serilog.Settings.Configuration" Version="3.4.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="2.1.10" />
<PackageVersion Include="SixLabors.ImageSharp.Web" Version="2.0.2" />
<PackageVersion Include="Smidge.InMemory" Version="4.3.0" />
<PackageVersion Include="Smidge.Nuglify" Version="4.2.1" />
<PackageVersion Include="System.IO.FileSystem.AccessControl" Version="5.0.0" />
<PackageVersion Include="System.Security.Cryptography.Pkcs" Version="6.0.4" />
<PackageVersion Include="System.Threading.Tasks.Dataflow" Version="6.0.0" />
<PackageVersion Include="System.ComponentModel.Annotations" Version="5.0.0" />
<PackageVersion Include="System.Reflection.Emit.Lightweight" Version="4.7.0" />
<PackageVersion Include="System.Runtime.Caching" Version="6.0.0" />
<PackageVersion Include="Umbraco.CSharpTest.Net.Collections" Version="14.906.1403.1085" />
<!-- Add dependencies that we force an update to, even that we do not use them explicitly and they seems to be taken from the framework instead of from Nuget -->
<PackageVersion Include="System.Net.Http" Version="4.3.4" />
<PackageVersion Include="System.Security.Cryptography.Xml" Version="6.0.1" />
<PackageVersion Include="System.Text.RegularExpressions" Version="4.3.1" />
</ItemGroup>
</Project>
+152
View File
@@ -0,0 +1,152 @@
// Copyright (c) Umbraco.
// See LICENSE for more details.
using Umbraco.Cms.Core.Configuration;
using Umbraco.Cms.Core.Configuration.Models;
using Umbraco.Deploy.Core.Configuration.DebugConfiguration;
using Umbraco.Deploy.Core.Configuration.DeployConfiguration;
using Umbraco.Deploy.Core.Configuration.DeployProjectConfiguration;
using Umbraco.Forms.Core.Configuration;
using SecuritySettings = Umbraco.Cms.Core.Configuration.Models.SecuritySettings;
namespace JsonSchema
{
internal class AppSettings
{
/// <summary>
/// Gets or sets the Umbraco
/// </summary>
public UmbracoDefinition? Umbraco { get; set; }
/// <summary>
/// Configuration of Umbraco CMS and packages
/// </summary>
internal class UmbracoDefinition
{
// ReSharper disable once InconsistentNaming
public CmsDefinition? CMS { get; set; }
public FormsDefinition? Forms { get; set; }
public DeployDefinition? Deploy { get; set; }
/// <summary>
/// Configurations for the Umbraco CMS
/// </summary>
public class CmsDefinition
{
public ContentSettings? Content { get; set; }
public CoreDebugSettings? Debug { get; set; }
public ExceptionFilterSettings? ExceptionFilter { get; set; }
public ModelsBuilderSettings? ModelsBuilder { get; set; }
public GlobalSettings? Global { get; set; }
public HealthChecksSettings? HealthChecks { get; set; }
public HostingSettings? Hosting { get; set; }
public ImagingSettings? Imaging { get; set; }
public IndexCreatorSettings? Examine { get; set; }
public IndexingSettings? Indexing { get; set; }
public KeepAliveSettings? KeepAlive { get; set; }
public LoggingSettings? Logging { get; set; }
public NuCacheSettings? NuCache { get; set; }
public RequestHandlerSettings? RequestHandler { get; set; }
public RuntimeSettings? Runtime { get; set; }
public SecuritySettings? Security { get; set; }
public TourSettings? Tours { get; set; }
public TypeFinderSettings? TypeFinder { get; set; }
public WebRoutingSettings? WebRouting { get; set; }
public UmbracoPluginSettings? Plugins { get; set; }
public UnattendedSettings? Unattended { get; set; }
public RichTextEditorSettings? RichTextEditor { get; set; }
public RuntimeMinificationSettings? RuntimeMinification { get; set; }
public BasicAuthSettings? BasicAuth { get; set; }
public PackageMigrationSettings? PackageMigration { get; set; }
public LegacyPasswordMigrationSettings? LegacyPasswordMigration { get; set; }
public ContentDashboardSettings? ContentDashboard { get; set; }
public HelpPageSettings? HelpPage { get; set; }
public InstallDefaultData? InstallDefaultData { get; set; }
public DataTypesSettings? DataTypes { get; set; }
public MarketplaceSettings? Marketplace { get; set; }
}
/// <summary>
/// Configurations for the Umbraco CMS InstallDefaultData configuration.
/// </summary>
public class InstallDefaultData
{
public InstallDefaultDataSettings? Languages { get; set; }
public InstallDefaultDataSettings? DataTypes { get; set; }
public InstallDefaultDataSettings? MediaTypes { get; set; }
public InstallDefaultDataSettings? MemberTypes { get; set; }
}
/// <summary>
/// Configurations for the Umbraco Forms package to Umbraco CMS
/// </summary>
public class FormsDefinition
{
public FormDesignSettings? FormDesign { get; set; }
public PackageOptionSettings? Options { get; set; }
public Umbraco.Forms.Core.Configuration.SecuritySettings? Security { get; set; }
public FieldTypesDefinition? FieldTypes { get; set; }
/// <summary>
/// Configurations for the Umbraco Forms Field Types
/// </summary>
public class FieldTypesDefinition
{
public DatePickerSettings? DatePicker { get; set; }
public Recaptcha2Settings? Recaptcha2 { get; set; }
public Recaptcha3Settings? Recaptcha3 { get; set; }
}
}
/// <summary>
/// Configurations for the Umbraco Deploy package to Umbraco CMS
/// </summary>
public class DeployDefinition
{
public DeploySettings? Settings { get; set; }
public DeployProjectConfig? Project { get; set; }
public DebugSettings? Debug { get; set; }
}
}
}
}
@@ -3,6 +3,7 @@
<OutputType>Exe</OutputType>
<IsPackable>false</IsPackable>
<EnablePackageValidation>false</EnablePackageValidation>
<ManagePackageVersionsCentrally>false</ManagePackageVersionsCentrally>
</PropertyGroup>
<ItemGroup>
@@ -11,6 +12,8 @@
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\Umbraco.Core\Umbraco.Core.csproj" />
<ProjectReference Include="..\Umbraco.Core\Umbraco.Core.csproj" />
<PackageReference Include="Umbraco.Deploy.Core" Version="10.4.0" />
<PackageReference Include="Umbraco.Forms.Core" Version="10.5.4" />
</ItemGroup>
</Project>
@@ -0,0 +1,13 @@
// Copyright (c) Umbraco.
// See LICENSE for more details.
using System;
using NJsonSchema.Generation;
namespace JsonSchema
{
internal class NamespacePrefixedSchemaNameGenerator : DefaultSchemaNameGenerator
{
public override string Generate(Type type) => type.Namespace?.Replace(".", string.Empty) + base.Generate(type);
}
}
+13
View File
@@ -0,0 +1,13 @@
// Copyright (c) Umbraco.
// See LICENSE for more details.
using CommandLine;
namespace JsonSchema
{
internal class Options
{
[Option('o', "outputFile", Required = false, HelpText = "Set path of the output file.", Default = "appsettings-schema.json")]
public string OutputFile { get; set; } = null!;
}
}
+41
View File
@@ -0,0 +1,41 @@
// Copyright (c) Umbraco.
// See LICENSE for more details.
using System;
using System.IO;
using System.Threading.Tasks;
using CommandLine;
namespace JsonSchema
{
internal class Program
{
public static async Task Main(string[] args)
{
try
{
await Parser.Default.ParseArguments<Options>(args)
.WithParsedAsync(Execute);
}
catch (Exception e)
{
Console.WriteLine(e);
throw;
}
}
private static async Task Execute(Options options)
{
var generator = new UmbracoJsonSchemaGenerator();
var schema = await generator.Generate();
var path = Path.GetFullPath(Path.Combine(Environment.CurrentDirectory, options.OutputFile));
Console.WriteLine("Path to use {0}", path);
Directory.CreateDirectory(Path.GetDirectoryName(path)!);
Console.WriteLine("Ensured directory exists");
await File.WriteAllTextAsync(path, schema);
Console.WriteLine("File written at {0}", path);
}
}
}
@@ -0,0 +1,58 @@
// Copyright (c) Umbraco.
// See LICENSE for more details.
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using NJsonSchema.Generation;
using Umbraco.Cms.Core.Configuration.Models;
namespace JsonSchema
{
/// <summary>
/// Generator of the JsonSchema for AppSettings.json including A specific Umbraco version.
/// </summary>
public class UmbracoJsonSchemaGenerator
{
private static readonly HttpClient s_client = new ();
private readonly JsonSchemaGenerator _innerGenerator;
/// <summary>
/// Initializes a new instance of the <see cref="UmbracoJsonSchemaGenerator" /> class.
/// </summary>
public UmbracoJsonSchemaGenerator()
=> _innerGenerator = new JsonSchemaGenerator(new UmbracoJsonSchemaGeneratorSettings());
/// <summary>
/// Generates a json representing the JsonSchema for AppSettings.json including A specific Umbraco version..
/// </summary>
public async Task<string> Generate()
{
JObject umbracoSchema = GenerateUmbracoSchema();
JObject officialSchema = await GetOfficialAppSettingsSchema();
officialSchema.Merge(umbracoSchema);
return officialSchema.ToString();
}
private async Task<JObject> GetOfficialAppSettingsSchema()
{
HttpResponseMessage response = await s_client.GetAsync("https://json.schemastore.org/appsettings.json")
.ConfigureAwait(false);
var result = await response.Content.ReadAsStringAsync();
return JsonConvert.DeserializeObject<JObject>(result)!;
}
private JObject GenerateUmbracoSchema()
{
NJsonSchema.JsonSchema schema = _innerGenerator.Generate(typeof(AppSettings));
// TODO: when the "UmbracoPath" setter is removed from "GlobalSettings" (scheduled for V12), remove this line as well
schema.Definitions["UmbracoCmsCoreConfigurationModelsGlobalSettings"]?.Properties?.Remove(nameof(GlobalSettings.UmbracoPath));
return JsonConvert.DeserializeObject<JObject>(schema.ToJson())!;
}
}
}
@@ -0,0 +1,43 @@
// Copyright (c) Umbraco.
// See LICENSE for more details.
using System;
using System.Collections.Generic;
using System.Linq;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
using Newtonsoft.Json.Serialization;
using NJsonSchema.Generation;
namespace JsonSchema
{
/// <inheritdoc />
public class UmbracoJsonSchemaGeneratorSettings : JsonSchemaGeneratorSettings
{
/// <summary>
/// Initializes a new instance of the <see cref="UmbracoJsonSchemaGeneratorSettings"/> class.
/// </summary>
public UmbracoJsonSchemaGeneratorSettings()
{
AlwaysAllowAdditionalObjectProperties = true;
SerializerSettings = new JsonSerializerSettings()
{
ContractResolver = new WritablePropertiesOnlyResolver()
};
DefaultReferenceTypeNullHandling = ReferenceTypeNullHandling.NotNull;
SchemaNameGenerator = new NamespacePrefixedSchemaNameGenerator();
SerializerSettings.Converters.Add(new StringEnumConverter());
IgnoreObsoleteProperties = true;
GenerateExamples = true;
}
private class WritablePropertiesOnlyResolver : DefaultContractResolver
{
protected override IList<JsonProperty> CreateProperties(Type type, MemberSerialization memberSerialization)
{
IList<JsonProperty> props = base.CreateProperties(type, memberSerialization);
return props.Where(p => p.Writable).ToList();
}
}
}
}
@@ -1,9 +0,0 @@
namespace Umbraco.Cms.Api.Common.Attributes;
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false)]
public class MapToApiAttribute : Attribute
{
public MapToApiAttribute(string apiName) => ApiName = apiName;
public string ApiName { get; }
}
@@ -1,12 +0,0 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options;
namespace Umbraco.Cms.Api.Common.Configuration;
public class ConfigureApiBehaviorOptions : IConfigureOptions<ApiBehaviorOptions>
{
public void Configure(ApiBehaviorOptions options) =>
// disable ProblemDetails as default result type for every non-success response (i.e. 404)
// - see https://learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.mvc.apibehavioroptions.suppressmapclienterrors
options.SuppressMapClientErrors = true;
}
@@ -1,31 +0,0 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Umbraco.Cms.Api.Common.Json;
namespace Umbraco.Cms.Api.Common.Configuration;
public class ConfigureMvcJsonOptions : IConfigureOptions<MvcOptions>
{
private readonly string _jsonOptionsName;
private readonly IOptionsMonitor<JsonOptions> _jsonOptions;
private readonly ILoggerFactory _loggerFactory;
public ConfigureMvcJsonOptions(
string jsonOptionsName,
IOptionsMonitor<JsonOptions> jsonOptions,
ILoggerFactory loggerFactory)
{
_jsonOptionsName = jsonOptionsName;
_jsonOptions = jsonOptions;
_loggerFactory = loggerFactory;
}
public void Configure(MvcOptions options)
{
JsonOptions jsonOptions = _jsonOptions.Get(_jsonOptionsName);
ILogger<NamedSystemTextJsonInputFormatter> logger = _loggerFactory.CreateLogger<NamedSystemTextJsonInputFormatter>();
options.InputFormatters.Insert(0, new NamedSystemTextJsonInputFormatter(_jsonOptionsName, jsonOptions, logger));
options.OutputFormatters.Insert(0, new NamedSystemTextJsonOutputFormatter(_jsonOptionsName, jsonOptions.JsonSerializerOptions));
}
}
@@ -1,65 +0,0 @@
using Asp.Versioning;
using Microsoft.AspNetCore.Mvc.ApiExplorer;
using Microsoft.AspNetCore.Mvc.Controllers;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using Microsoft.OpenApi.Models;
using Swashbuckle.AspNetCore.SwaggerGen;
using Umbraco.Cms.Api.Common.OpenApi;
using Umbraco.Extensions;
namespace Umbraco.Cms.Api.Common.Configuration;
public class ConfigureUmbracoSwaggerGenOptions : IConfigureOptions<SwaggerGenOptions>
{
private readonly IOptions<ApiVersioningOptions> _apiVersioningOptions;
private readonly IOperationIdSelector _operationIdSelector;
private readonly ISchemaIdSelector _schemaIdSelector;
public ConfigureUmbracoSwaggerGenOptions(
IOptions<ApiVersioningOptions> apiVersioningOptions,
IOperationIdSelector operationIdSelector,
ISchemaIdSelector schemaIdSelector)
{
_apiVersioningOptions = apiVersioningOptions;
_operationIdSelector = operationIdSelector;
_schemaIdSelector = schemaIdSelector;
}
public void Configure(SwaggerGenOptions swaggerGenOptions)
{
swaggerGenOptions.SwaggerDoc(
DefaultApiConfiguration.ApiName,
new OpenApiInfo
{
Title = "Default API",
Version = "Latest",
Description = "All endpoints not defined under specific APIs"
});
swaggerGenOptions.CustomOperationIds(description => _operationIdSelector.OperationId(description, _apiVersioningOptions.Value));
swaggerGenOptions.DocInclusionPredicate((name, api) =>
{
if (string.IsNullOrWhiteSpace(api.GroupName))
{
return false;
}
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.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["action"]}_{apiDesc.HttpMethod}";
}
@@ -1,6 +0,0 @@
namespace Umbraco.Cms.Api.Common.Configuration;
internal static class DefaultApiConfiguration
{
public const string ApiName = "default";
}
@@ -1,22 +0,0 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Umbraco.Cms.Api.Common.Configuration;
namespace Umbraco.Cms.Api.Common.DependencyInjection;
public static class MvcBuilderExtensions
{
public static IMvcBuilder AddJsonOptions(this IMvcBuilder builder, string settingsName, Action<JsonOptions> configure)
{
builder.Services.Configure(settingsName, configure);
builder.Services.AddSingleton<IConfigureOptions<MvcOptions>>(provider =>
{
IOptionsMonitor<JsonOptions> options = provider.GetRequiredService<IOptionsMonitor<JsonOptions>>();
ILoggerFactory loggerFactory = provider.GetRequiredService<ILoggerFactory>();
return new ConfigureMvcJsonOptions(settingsName, options, loggerFactory);
});
return builder;
}
}
@@ -1,23 +0,0 @@
using Microsoft.Extensions.DependencyInjection;
using Umbraco.Cms.Api.Common.Configuration;
using Umbraco.Cms.Api.Common.OpenApi;
using Umbraco.Cms.Api.Common.Serialization;
using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Web.Common.ApplicationBuilder;
namespace Umbraco.Cms.Api.Common.DependencyInjection;
public static class UmbracoBuilderApiExtensions
{
public static IUmbracoBuilder AddUmbracoApiOpenApiUI(this IUmbracoBuilder builder)
{
builder.Services.AddSwaggerGen();
builder.Services.ConfigureOptions<ConfigureUmbracoSwaggerGenOptions>();
builder.Services.AddSingleton<IUmbracoJsonTypeInfoResolver, UmbracoJsonTypeInfoResolver>();
builder.Services.AddSingleton<IOperationIdSelector, OperationIdSelector>();
builder.Services.AddSingleton<ISchemaIdSelector, SchemaIdSelector>();
builder.Services.Configure<UmbracoPipelineOptions>(options => options.AddFilter(new SwaggerRouteTemplatePipelineFilter("UmbracoApiCommon")));
return builder;
}
}
@@ -1,32 +0,0 @@
using System.Reflection;
using Asp.Versioning;
using Umbraco.Cms.Api.Common.Attributes;
using Umbraco.Cms.Api.Common.Configuration;
namespace Umbraco.Extensions;
public static class MethodInfoApiCommonExtensions
{
public static string? GetMapToApiVersionAttributeValue(this MethodInfo methodInfo)
{
MapToApiVersionAttribute[] mapToApis = methodInfo.GetCustomAttributes(typeof(MapToApiVersionAttribute), inherit: true).Cast<MapToApiVersionAttribute>().ToArray();
return string.Join("|", mapToApis.SelectMany(x=>x.Versions));
}
public static string? GetMapToApiAttributeValue(this MethodInfo methodInfo)
{
MapToApiAttribute[] mapToApis = (methodInfo.DeclaringType?.GetCustomAttributes(typeof(MapToApiAttribute), inherit: true) ?? Array.Empty<object>()).Cast<MapToApiAttribute>().ToArray();
return mapToApis.SingleOrDefault()?.ApiName;
}
public static bool HasMapToApiAttribute(this MethodInfo methodInfo, string apiName)
{
var value = methodInfo.GetMapToApiAttributeValue();
return value == apiName
|| (value is null && apiName == DefaultApiConfiguration.ApiName);
}
}
@@ -1,9 +0,0 @@
namespace Umbraco.Cms.Api.Common.Filters;
[AttributeUsage(AttributeTargets.Class)]
public class JsonOptionsNameAttribute : Attribute
{
public JsonOptionsNameAttribute(string jsonOptionsName) => JsonOptionsName = jsonOptionsName;
public string JsonOptionsName { get; }
}
@@ -1,10 +0,0 @@
using Microsoft.AspNetCore.Http;
using Umbraco.Cms.Api.Common.Filters;
namespace Umbraco.Cms.Api.Common.Json;
public static class HttpContextJsonExtensions
{
public static string? CurrentJsonOptionsName(this HttpContext context)
=> context.GetEndpoint()?.Metadata.GetMetadata<JsonOptionsNameAttribute>()?.JsonOptionsName;
}
@@ -1,31 +0,0 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Formatters;
using Microsoft.Extensions.Logging;
namespace Umbraco.Cms.Api.Common.Json;
internal class NamedSystemTextJsonInputFormatter : SystemTextJsonInputFormatter
{
private readonly string _jsonOptionsName;
public NamedSystemTextJsonInputFormatter(string jsonOptionsName, JsonOptions options, ILogger<NamedSystemTextJsonInputFormatter> logger)
: base(options, logger) =>
_jsonOptionsName = jsonOptionsName;
public override bool CanRead(InputFormatterContext context)
=> context.HttpContext.CurrentJsonOptionsName() == _jsonOptionsName && base.CanRead(context);
public override async Task<InputFormatterResult> ReadAsync(InputFormatterContext context)
{
try
{
return await base.ReadAsync(context);
}
catch (NotSupportedException exception)
{
// This happens when trying to deserialize to an interface, without sending the $type as part of the request
context.ModelState.TryAddModelException(string.Empty, new InputFormatterException(exception.Message, exception));
return await InputFormatterResult.FailureAsync();
}
}
}
@@ -1,18 +0,0 @@
using System.Text.Json;
using Microsoft.AspNetCore.Mvc.Formatters;
namespace Umbraco.Cms.Api.Common.Json;
internal class NamedSystemTextJsonOutputFormatter : SystemTextJsonOutputFormatter
{
private readonly string _jsonOptionsName;
public NamedSystemTextJsonOutputFormatter(string jsonOptionsName, JsonSerializerOptions jsonSerializerOptions) : base(jsonSerializerOptions)
{
_jsonOptionsName = jsonOptionsName;
}
public override bool CanWriteResult(OutputFormatterCanWriteContext context)
=> context.HttpContext.CurrentJsonOptionsName() == _jsonOptionsName && base.CanWriteResult(context);
}
@@ -1,23 +0,0 @@
using System.Reflection;
using System.Runtime.Serialization;
using Microsoft.OpenApi.Any;
using Microsoft.OpenApi.Models;
using Swashbuckle.AspNetCore.SwaggerGen;
namespace Umbraco.Cms.Api.Common.OpenApi;
public class EnumSchemaFilter : ISchemaFilter
{
public void Apply(OpenApiSchema model, SchemaFilterContext context)
{
if (context.Type.IsEnum)
{
model.Enum.Clear();
foreach (var name in Enum.GetNames(context.Type))
{
var actualName = context.Type.GetField(name)?.GetCustomAttribute<EnumMemberAttribute>()?.Value ?? name;
model.Enum.Add(new OpenApiString(actualName));
}
}
}
}
@@ -1,9 +0,0 @@
using Asp.Versioning;
using Microsoft.AspNetCore.Mvc.ApiExplorer;
namespace Umbraco.Cms.Api.Common.OpenApi;
public interface IOperationIdSelector
{
string? OperationId(ApiDescription apiDescription, ApiVersioningOptions apiVersioningOptions);
}
@@ -1,6 +0,0 @@
namespace Umbraco.Cms.Api.Common.OpenApi;
public interface ISchemaIdSelector
{
string SchemaId(Type type);
}
@@ -1,48 +0,0 @@
using Microsoft.OpenApi.Models;
using Swashbuckle.AspNetCore.SwaggerGen;
using Umbraco.Extensions;
namespace Umbraco.Cms.Api.Common.OpenApi;
/// <summary>
/// This filter explicitly removes all other mime types than application/json from a named OpenAPI document when application/json is accepted.
/// </summary>
public class MimeTypeDocumentFilter : IDocumentFilter
{
private readonly string _documentName;
public MimeTypeDocumentFilter(string documentName) => _documentName = documentName;
public void Apply(OpenApiDocument swaggerDoc, DocumentFilterContext context)
{
if (context.DocumentName != _documentName)
{
return;
}
OpenApiOperation[] operations = swaggerDoc.Paths
.SelectMany(path => path.Value.Operations.Values)
.ToArray();
void RemoveUnwantedMimeTypes(IDictionary<string, OpenApiMediaType> content)
{
if (content.ContainsKey("application/json"))
{
content.RemoveAll(r => r.Key != "application/json");
}
}
OpenApiRequestBody[] requestBodies = operations.Select(operation => operation.RequestBody).WhereNotNull().ToArray();
foreach (OpenApiRequestBody requestBody in requestBodies)
{
RemoveUnwantedMimeTypes(requestBody.Content);
}
OpenApiResponse[] responses = operations.SelectMany(operation => operation.Responses.Values).WhereNotNull().ToArray();
foreach (OpenApiResponse response in responses)
{
RemoveUnwantedMimeTypes(response.Content);
}
}
}
@@ -1,21 +0,0 @@
using System.Text.RegularExpressions;
namespace Umbraco.Cms.Api.Common.OpenApi;
/// <summary>
/// This is the regexes used to generate the operation IDs, the benefit of this being partial with GeneratedRegex
/// source generators is that it will be pre-compiled at startup
/// See: https://devblogs.microsoft.com/dotnet/regular-expression-improvements-in-dotnet-7/#source-generation for more info.
/// </summary>
internal static partial class OperationIdRegexes
{
// Your IDE may be showing errors here, this is because it's a new dotnet 7 feature (it's fixed in the EAP of Rider)
[GeneratedRegex(".*?\\/v[1-9]+/")]
public static partial Regex VersionPrefixRegex();
[GeneratedRegex("\\{(.*?)\\:?\\}")]
public static partial Regex TemplatePlaceholdersRegex();
[GeneratedRegex("[\\/\\-](\\w{1})")]
public static partial Regex ToCamelCaseRegex();
}
@@ -1,79 +0,0 @@
using Asp.Versioning;
using Microsoft.AspNetCore.Mvc.ApiExplorer;
using Microsoft.AspNetCore.Mvc.Controllers;
using Umbraco.Extensions;
namespace Umbraco.Cms.Api.Common.OpenApi;
public class OperationIdSelector : IOperationIdSelector
{
public virtual string? OperationId(ApiDescription apiDescription, ApiVersioningOptions apiVersioningOptions)
{
if (apiDescription.ActionDescriptor is not ControllerActionDescriptor controllerActionDescriptor
|| controllerActionDescriptor.ControllerTypeInfo.Namespace?.StartsWith("Umbraco.Cms.Api") is not true)
{
return null;
}
return UmbracoOperationId(apiDescription, apiVersioningOptions);
}
protected string? UmbracoOperationId(ApiDescription apiDescription, ApiVersioningOptions apiVersioningOptions)
{
if (apiDescription.ActionDescriptor is not ControllerActionDescriptor controllerActionDescriptor)
{
return null;
}
ApiVersion defaultVersion = apiVersioningOptions.DefaultApiVersion;
var httpMethod = apiDescription.HttpMethod?.ToLower().ToFirstUpper() ?? "Get";
// if the route info "Name" is supplied we'll use this explicitly as the operation ID
// - usage example: [HttpGet("my-api/route}", Name = "MyCustomRoute")]
if (string.IsNullOrWhiteSpace(apiDescription.ActionDescriptor.AttributeRouteInfo?.Name) == false)
{
var explicitOperationId = apiDescription.ActionDescriptor.AttributeRouteInfo!.Name;
return explicitOperationId.InvariantStartsWith(httpMethod)
? explicitOperationId
: $"{httpMethod}{explicitOperationId}";
}
var relativePath = apiDescription.RelativePath;
if (string.IsNullOrWhiteSpace(relativePath))
{
throw new Exception(
$"There is no relative path for controller action {apiDescription.ActionDescriptor.RouteValues["controller"]}");
}
// Remove the prefixed base path with version, e.g. /umbraco/management/api/v1/tracked-reference/{id} => tracked-reference/{id}
var unprefixedRelativePath = OperationIdRegexes
.VersionPrefixRegex()
.Replace(relativePath, string.Empty);
// Remove template placeholders, e.g. tracked-reference/{id} => tracked-reference/Id
var formattedOperationId = OperationIdRegexes
.TemplatePlaceholdersRegex()
.Replace(unprefixedRelativePath, m => $"By{m.Groups[1].Value.ToFirstUpper()}");
// Remove dashes (-) and slashes (/) and convert the following letter to uppercase with
// the word "By" in front, e.g. tracked-reference/Id => TrackedReferenceById
formattedOperationId = OperationIdRegexes
.ToCamelCaseRegex()
.Replace(formattedOperationId, m => m.Groups[1].Value.ToUpper());
//Get map to version attribute
string? version = null;
var versionAttributeValue = controllerActionDescriptor.MethodInfo.GetMapToApiVersionAttributeValue();
// We only wanna add a version, if it is not the default one.
if (string.Equals(versionAttributeValue, defaultVersion.ToString()) == false)
{
version = versionAttributeValue;
}
// Return the operation ID with the formatted http method verb in front, e.g. GetTrackedReferenceById
return $"{httpMethod}{formattedOperationId.ToFirstUpper()}{version}";
}
}
@@ -1,36 +0,0 @@
using System.Text.RegularExpressions;
using Umbraco.Extensions;
namespace Umbraco.Cms.Api.Common.OpenApi;
public class SchemaIdSelector : ISchemaIdSelector
{
public virtual string SchemaId(Type type)
=> type.Namespace?.StartsWith("Umbraco.Cms") is true ? UmbracoSchemaId(type) : type.Name;
protected string UmbracoSchemaId(Type type)
{
string SanitizedTypeName(Type t) => t.Name
// first grab the "non generic" part of any generic type name (i.e. "PagedViewModel`1" becomes "PagedViewModel")
.Split('`').First()
// then remove the "ViewModel" postfix from type names
.TrimEnd("ViewModel");
var name = SanitizedTypeName(type);
if (type.IsGenericType)
{
// append the generic type names, ultimately turning i.e. "PagedViewModel<RelationItemViewModel>" into "PagedRelationItem"
name = $"{name}{string.Join(string.Empty, type.GenericTypeArguments.Select(SanitizedTypeName))}";
}
if (name.EndsWith("Model") == false)
{
// because some models names clash with common classes in TypeScript (i.e. Document),
// we need to add a "Model" postfix to all models
name = $"{name}Model";
}
// make absolutely sure we don't pass any invalid named by removing all non-word chars
return Regex.Replace(name, @"[^\w]", string.Empty);
}
}
@@ -1,67 +0,0 @@
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Options;
using Microsoft.OpenApi.Models;
using Swashbuckle.AspNetCore.SwaggerGen;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Configuration.Models;
using Umbraco.Cms.Web.Common.ApplicationBuilder;
using Umbraco.Extensions;
using IHostingEnvironment = Umbraco.Cms.Core.Hosting.IHostingEnvironment;
namespace Umbraco.Cms.Api.Common.OpenApi;
public class SwaggerRouteTemplatePipelineFilter : UmbracoPipelineFilter
{
public SwaggerRouteTemplatePipelineFilter(string name)
: base(name)
=> PostPipeline = PostPipelineAction;
private void PostPipelineAction(IApplicationBuilder applicationBuilder)
{
if (SwaggerIsEnabled(applicationBuilder) is false)
{
return;
}
IOptions<SwaggerGenOptions> swaggerGenOptions = applicationBuilder.ApplicationServices.GetRequiredService<IOptions<SwaggerGenOptions>>();
applicationBuilder.UseSwagger(swaggerOptions =>
{
swaggerOptions.RouteTemplate = SwaggerRouteTemplate(applicationBuilder);
});
applicationBuilder.UseSwaggerUI(
swaggerUiOptions =>
{
swaggerUiOptions.RoutePrefix = SwaggerUiRoutePrefix(applicationBuilder);
foreach ((var name, OpenApiInfo? apiInfo) in swaggerGenOptions.Value.SwaggerGeneratorOptions.SwaggerDocs
.OrderBy(x => x.Value.Title))
{
swaggerUiOptions.SwaggerEndpoint($"{name}/swagger.json", $"{apiInfo.Title}");
}
});
}
protected virtual bool SwaggerIsEnabled(IApplicationBuilder applicationBuilder)
{
IWebHostEnvironment webHostEnvironment = applicationBuilder.ApplicationServices.GetRequiredService<IWebHostEnvironment>();
return webHostEnvironment.IsProduction() is false;
}
protected virtual string SwaggerRouteTemplate(IApplicationBuilder applicationBuilder)
=> $"{GetUmbracoPath(applicationBuilder).TrimStart(Constants.CharArrays.ForwardSlash)}/swagger/{{documentName}}/swagger.json";
protected virtual string SwaggerUiRoutePrefix(IApplicationBuilder applicationBuilder)
=> $"{GetUmbracoPath(applicationBuilder).TrimStart(Constants.CharArrays.ForwardSlash)}/swagger";
private string GetUmbracoPath(IApplicationBuilder applicationBuilder)
{
GlobalSettings settings = applicationBuilder.ApplicationServices.GetRequiredService<IOptions<GlobalSettings>>().Value;
IHostingEnvironment hostingEnvironment = applicationBuilder.ApplicationServices.GetRequiredService<IHostingEnvironment>();
return settings.GetBackOfficePath(hostingEnvironment);
}
}
@@ -1,8 +0,0 @@
using System.Text.Json.Serialization.Metadata;
namespace Umbraco.Cms.Api.Common.Serialization;
public interface IUmbracoJsonTypeInfoResolver : IJsonTypeInfoResolver
{
IEnumerable<Type> FindSubTypes(Type type);
}
@@ -1,86 +0,0 @@
using System.Collections.Concurrent;
using System.Text.Json;
using System.Text.Json.Serialization.Metadata;
using Umbraco.Cms.Core.Composing;
namespace Umbraco.Cms.Api.Common.Serialization;
public sealed class UmbracoJsonTypeInfoResolver : DefaultJsonTypeInfoResolver, IUmbracoJsonTypeInfoResolver
{
private readonly ITypeFinder _typeFinder;
private readonly ConcurrentDictionary<Type, ISet<Type>> _subTypesCache = new ConcurrentDictionary<Type, ISet<Type>>();
public UmbracoJsonTypeInfoResolver(ITypeFinder typeFinder)
{
_typeFinder = typeFinder;
}
public IEnumerable<Type> FindSubTypes(Type type)
{
if (_subTypesCache.TryGetValue(type, out ISet<Type>? cachedResult))
{
return cachedResult;
}
var result = _typeFinder.FindClassesOfType(type).ToHashSet();
_subTypesCache.TryAdd(type, result);
return result;
}
public override JsonTypeInfo GetTypeInfo(Type type, JsonSerializerOptions options)
{
JsonTypeInfo result = base.GetTypeInfo(type, options);
if (type.IsInterface)
{
return GetTypeInfoForInterface(result, type, options);
}
else
{
return GetTypeInfoForClass(result, type, options);
}
}
private JsonTypeInfo GetTypeInfoForClass(JsonTypeInfo result, Type type, JsonSerializerOptions options)
{
if (result.Kind != JsonTypeInfoKind.Object || !type.GetInterfaces().Any())
{
return result;
}
JsonPolymorphismOptions jsonPolymorphismOptions = result.PolymorphismOptions ?? new JsonPolymorphismOptions();
jsonPolymorphismOptions.DerivedTypes.Add(new JsonDerivedType(type, type.Name));
result.PolymorphismOptions = jsonPolymorphismOptions;
return result;
}
private JsonTypeInfo GetTypeInfoForInterface(JsonTypeInfo result, Type type, JsonSerializerOptions options)
{
IEnumerable<Type> subTypes = FindSubTypes(type);
if (!subTypes.Any())
{
return result;
}
JsonPolymorphismOptions jsonPolymorphismOptions = result.PolymorphismOptions ?? new JsonPolymorphismOptions();
IEnumerable<Type> knownSubTypes = jsonPolymorphismOptions.DerivedTypes.Select(x => x.DerivedType);
IEnumerable<Type> subTypesToAdd = subTypes.Except(knownSubTypes);
foreach (Type subType in subTypesToAdd)
{
jsonPolymorphismOptions.DerivedTypes.Add(new JsonDerivedType(
subType,
subType.Name ?? string.Empty));
}
result.PolymorphismOptions = jsonPolymorphismOptions;
return result;
}
}
@@ -1,21 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<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>
<ItemGroup>
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.5.0" />
<PackageReference Include="OpenIddict.Abstractions" Version="4.5.0" />
<PackageReference Include="OpenIddict.AspNetCore" Version="4.5.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Umbraco.Core\Umbraco.Core.csproj" />
<ProjectReference Include="..\Umbraco.Web.Common\Umbraco.Web.Common.csproj" />
</ItemGroup>
</Project>
@@ -1,12 +0,0 @@
using Microsoft.AspNetCore.Http;
using Umbraco.Cms.Core.DeliveryApi;
namespace Umbraco.Cms.Api.Delivery.Accessors;
internal sealed class RequestContextOutputExpansionStrategyAccessor : RequestContextServiceAccessorBase<IOutputExpansionStrategy>, IOutputExpansionStrategyAccessor
{
public RequestContextOutputExpansionStrategyAccessor(IHttpContextAccessor httpContextAccessor)
: base(httpContextAccessor)
{
}
}
@@ -1,12 +0,0 @@
using Microsoft.AspNetCore.Http;
using Umbraco.Cms.Core.DeliveryApi;
namespace Umbraco.Cms.Api.Delivery.Accessors;
internal sealed class RequestContextRequestStartItemProviderAccessor : RequestContextServiceAccessorBase<IRequestStartItemProvider>, IRequestStartItemProviderAccessor
{
public RequestContextRequestStartItemProviderAccessor(IHttpContextAccessor httpContextAccessor)
: base(httpContextAccessor)
{
}
}
@@ -1,20 +0,0 @@
using System.Diagnostics.CodeAnalysis;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
namespace Umbraco.Cms.Api.Delivery.Accessors;
public abstract class RequestContextServiceAccessorBase<T>
where T : class
{
private readonly IHttpContextAccessor _httpContextAccessor;
protected RequestContextServiceAccessorBase(IHttpContextAccessor httpContextAccessor)
=> _httpContextAccessor = httpContextAccessor;
public bool TryGetValue([NotNullWhen(true)] out T? requestStartNodeService)
{
requestStartNodeService = _httpContextAccessor.HttpContext?.RequestServices.GetService<T>();
return requestStartNodeService is not null;
}
}
@@ -1,28 +0,0 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using Microsoft.OpenApi.Models;
using Swashbuckle.AspNetCore.SwaggerGen;
using Umbraco.Cms.Api.Common.OpenApi;
using Umbraco.Cms.Api.Delivery.Filters;
namespace Umbraco.Cms.Api.Delivery.Configuration;
public class ConfigureUmbracoDeliveryApiSwaggerGenOptions: IConfigureOptions<SwaggerGenOptions>
{
public void Configure(SwaggerGenOptions swaggerGenOptions)
{
swaggerGenOptions.SwaggerDoc(
DeliveryApiConfiguration.ApiName,
new OpenApiInfo
{
Title = DeliveryApiConfiguration.ApiTitle,
Version = "Latest",
Description = $"You can find out more about the {DeliveryApiConfiguration.ApiTitle} in [the documentation]({DeliveryApiConfiguration.ApiDocumentationArticleLink})."
});
swaggerGenOptions.DocumentFilter<MimeTypeDocumentFilter>(DeliveryApiConfiguration.ApiName);
swaggerGenOptions.OperationFilter<SwaggerDocumentationFilter>();
swaggerGenOptions.ParameterFilter<SwaggerDocumentationFilter>();
}
}
@@ -1,10 +0,0 @@
namespace Umbraco.Cms.Api.Delivery.Configuration;
internal static class DeliveryApiConfiguration
{
internal const string ApiTitle = "Umbraco Delivery API";
internal const string ApiName = "delivery";
internal const string ApiDocumentationArticleLink = "https://docs.umbraco.com/umbraco-cms/v/12.latest/reference/content-delivery-api";
}
@@ -1,54 +0,0 @@
using Asp.Versioning;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Core.DeliveryApi;
using Umbraco.Cms.Core.Models.DeliveryApi;
using Umbraco.Cms.Core.Models.PublishedContent;
using Umbraco.Cms.Core.Services;
namespace Umbraco.Cms.Api.Delivery.Controllers;
[ApiVersion("1.0")]
public class ByIdContentApiController : ContentApiItemControllerBase
{
public ByIdContentApiController(
IApiPublishedContentCache apiPublishedContentCache,
IApiContentResponseBuilder apiContentResponseBuilder,
IPublicAccessService publicAccessService)
: base(apiPublishedContentCache, apiContentResponseBuilder, publicAccessService)
{
}
/// <summary>
/// Gets a content item by id.
/// </summary>
/// <param name="id">The unique identifier of the content item.</param>
/// <returns>The content item or not found result.</returns>
[HttpGet("item/{id:guid}")]
[MapToApiVersion("1.0")]
[ProducesResponseType(typeof(IApiContentResponse), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<IActionResult> ById(Guid id)
{
IPublishedContent? contentItem = ApiPublishedContentCache.GetById(id);
if (contentItem is null)
{
return NotFound();
}
if (IsProtected(contentItem))
{
return Unauthorized();
}
IApiContentResponse? apiContentResponse = ApiContentResponseBuilder.Build(contentItem);
if (apiContentResponse is null)
{
return NotFound();
}
return await Task.FromResult(Ok(apiContentResponse));
}
}
@@ -1,116 +0,0 @@
using System.Net;
using Asp.Versioning;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.DeliveryApi;
using Umbraco.Cms.Core.Models.DeliveryApi;
using Umbraco.Cms.Core.Models.PublishedContent;
using Umbraco.Cms.Core.Services;
using Umbraco.Extensions;
namespace Umbraco.Cms.Api.Delivery.Controllers;
[ApiVersion("1.0")]
public class ByRouteContentApiController : ContentApiItemControllerBase
{
private readonly IRequestRoutingService _requestRoutingService;
private readonly IRequestRedirectService _requestRedirectService;
private readonly IRequestPreviewService _requestPreviewService;
public ByRouteContentApiController(
IApiPublishedContentCache apiPublishedContentCache,
IApiContentResponseBuilder apiContentResponseBuilder,
IPublicAccessService publicAccessService,
IRequestRoutingService requestRoutingService,
IRequestRedirectService requestRedirectService,
IRequestPreviewService requestPreviewService)
: base(apiPublishedContentCache, apiContentResponseBuilder, publicAccessService)
{
_requestRoutingService = requestRoutingService;
_requestRedirectService = requestRedirectService;
_requestPreviewService = requestPreviewService;
}
/// <summary>
/// Gets a content item by route.
/// </summary>
/// <param name="path">The path to the content item.</param>
/// <remarks>
/// Optional URL segment for the root content item
/// can be added through the "start-item" header.
/// </remarks>
/// <returns>The content item or not found result.</returns>
[HttpGet("item/{*path}")]
[MapToApiVersion("1.0")]
[ProducesResponseType(typeof(IApiContentResponse), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<IActionResult> ByRoute(string path = "")
{
// OpenAPI does not allow reserved chars as "in:path" parameters, so clients based on the Swagger JSON will URL
// encode the path. Normally, ASP.NET Core handles that encoding with an automatic decoding - apparently just not
// for forward slashes, for whatever reason... so we need to deal with those. Hopefully this will be addressed in
// an upcoming version of ASP.NET Core.
// See also https://github.com/dotnet/aspnetcore/issues/11544
if (path.Contains("%2F", StringComparison.OrdinalIgnoreCase))
{
path = WebUtility.UrlDecode(path);
}
path = path.TrimStart("/");
path = path.Length == 0 ? "/" : path;
IPublishedContent? contentItem = GetContent(path);
if (contentItem is not null)
{
if (IsProtected(contentItem))
{
return Unauthorized();
}
return await Task.FromResult(Ok(ApiContentResponseBuilder.Build(contentItem)));
}
IApiContentRoute? redirectRoute = _requestRedirectService.GetRedirectRoute(path);
return redirectRoute != null
? RedirectTo(redirectRoute)
: NotFound();
}
private IPublishedContent? GetContent(string path)
=> path.StartsWith(Constants.DeliveryApi.Routing.PreviewContentPathPrefix)
? GetPreviewContent(path)
: GetPublishedContent(path);
private IPublishedContent? GetPublishedContent(string path)
{
var contentRoute = _requestRoutingService.GetContentRoute(path);
IPublishedContent? contentItem = ApiPublishedContentCache.GetByRoute(contentRoute);
return contentItem;
}
private IPublishedContent? GetPreviewContent(string path)
{
if (_requestPreviewService.IsPreview() is false)
{
return null;
}
if (Guid.TryParse(path.AsSpan(Constants.DeliveryApi.Routing.PreviewContentPathPrefix.Length).TrimEnd("/"), out Guid contentId) is false)
{
return null;
}
IPublishedContent? contentItem = ApiPublishedContentCache.GetById(contentId);
return contentItem;
}
private IActionResult RedirectTo(IApiContentRoute redirectRoute)
{
Response.Headers.Add("Location-Start-Item-Path", redirectRoute.StartItem.Path);
Response.Headers.Add("Location-Start-Item-Id", redirectRoute.StartItem.Id.ToString("D"));
return RedirectPermanent(redirectRoute.Path);
}
}
@@ -1,43 +0,0 @@
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Api.Common.Builders;
using Umbraco.Cms.Api.Delivery.Filters;
using Umbraco.Cms.Api.Delivery.Routing;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.DeliveryApi;
using Umbraco.Cms.Core.Services.OperationStatus;
namespace Umbraco.Cms.Api.Delivery.Controllers;
[VersionedDeliveryApiRoute("content")]
[ApiExplorerSettings(GroupName = "Content")]
[LocalizeFromAcceptLanguageHeader]
[ValidateStartItem]
public abstract class ContentApiControllerBase : DeliveryApiControllerBase
{
protected IApiPublishedContentCache ApiPublishedContentCache { get; }
protected IApiContentResponseBuilder ApiContentResponseBuilder { get; }
protected ContentApiControllerBase(IApiPublishedContentCache apiPublishedContentCache, IApiContentResponseBuilder apiContentResponseBuilder)
{
ApiPublishedContentCache = apiPublishedContentCache;
ApiContentResponseBuilder = apiContentResponseBuilder;
}
protected IActionResult ApiContentQueryOperationStatusResult(ApiContentQueryOperationStatus status) =>
status switch
{
ApiContentQueryOperationStatus.FilterOptionNotFound => BadRequest(new ProblemDetailsBuilder()
.WithTitle("Filter option not found")
.WithDetail("One of the attempted 'filter' options does not exist")
.Build()),
ApiContentQueryOperationStatus.SelectorOptionNotFound => BadRequest(new ProblemDetailsBuilder()
.WithTitle("Selector option not found")
.WithDetail("The attempted 'fetch' option does not exist")
.Build()),
ApiContentQueryOperationStatus.SortOptionNotFound => BadRequest(new ProblemDetailsBuilder()
.WithTitle("Sort option not found")
.WithDetail("One of the attempted 'sort' options does not exist")
.Build()),
};
}
@@ -1,22 +0,0 @@
using Umbraco.Cms.Core.DeliveryApi;
using Umbraco.Cms.Core.Models.PublishedContent;
using Umbraco.Cms.Core.Services;
namespace Umbraco.Cms.Api.Delivery.Controllers;
public abstract class ContentApiItemControllerBase : ContentApiControllerBase
{
private readonly IPublicAccessService _publicAccessService;
protected ContentApiItemControllerBase(
IApiPublishedContentCache apiPublishedContentCache,
IApiContentResponseBuilder apiContentResponseBuilder,
IPublicAccessService publicAccessService)
: base(apiPublishedContentCache, apiContentResponseBuilder)
=> _publicAccessService = publicAccessService;
// NOTE: we're going to test for protected content at item endpoint level, because the check has already been
// performed at content index time for the query endpoint and we don't want that extra overhead when
// returning multiple items.
protected bool IsProtected(IPublishedContent content) => _publicAccessService.IsProtected(content.Path);
}
@@ -1,17 +0,0 @@
using Asp.Versioning;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Api.Common.Attributes;
using Umbraco.Cms.Api.Common.Filters;
using Umbraco.Cms.Api.Delivery.Configuration;
using Umbraco.Cms.Api.Delivery.Filters;
using Umbraco.Cms.Core;
namespace Umbraco.Cms.Api.Delivery.Controllers;
[ApiController]
[DeliveryApiAccess]
[JsonOptionsName(Constants.JsonOptionsNames.DeliveryApi)]
[MapToApi(DeliveryApiConfiguration.ApiName)]
public abstract class DeliveryApiControllerBase : Controller
{
}
@@ -1,67 +0,0 @@
using Asp.Versioning;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Api.Common.ViewModels.Pagination;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.DeliveryApi;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.DeliveryApi;
using Umbraco.Cms.Core.Models.PublishedContent;
using Umbraco.Cms.Core.Services.OperationStatus;
using Umbraco.Extensions;
namespace Umbraco.Cms.Api.Delivery.Controllers;
[ApiVersion("1.0")]
public class QueryContentApiController : ContentApiControllerBase
{
private readonly IApiContentQueryService _apiContentQueryService;
public QueryContentApiController(
IApiPublishedContentCache apiPublishedContentCache,
IApiContentResponseBuilder apiContentResponseBuilderBuilder,
IApiContentQueryService apiContentQueryService)
: base(apiPublishedContentCache, apiContentResponseBuilderBuilder)
=> _apiContentQueryService = apiContentQueryService;
/// <summary>
/// Gets a paginated list of content item(s) from query.
/// </summary>
/// <param name="fetch">Optional fetch query parameter value.</param>
/// <param name="filter">Optional filter query parameters values.</param>
/// <param name="sort">Optional sort query parameters values.</param>
/// <param name="skip">The amount of items to skip.</param>
/// <param name="take">The amount of items to take.</param>
/// <returns>The paged result of the content item(s).</returns>
[HttpGet]
[MapToApiVersion("1.0")]
[ProducesResponseType(typeof(PagedViewModel<IApiContentResponse>), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<IActionResult> Query(
string? fetch,
[FromQuery] string[] filter,
[FromQuery] string[] sort,
int skip = 0,
int take = 10)
{
Attempt<PagedModel<Guid>, ApiContentQueryOperationStatus> queryAttempt = _apiContentQueryService.ExecuteQuery(fetch, filter, sort, skip, take);
if (queryAttempt.Success is false)
{
return ApiContentQueryOperationStatusResult(queryAttempt.Status);
}
PagedModel<Guid> pagedResult = queryAttempt.Result;
IEnumerable<IPublishedContent> contentItems = ApiPublishedContentCache.GetByIds(pagedResult.Items);
IApiContentResponse[] apiContentItems = contentItems.Select(ApiContentResponseBuilder.Build).WhereNotNull().ToArray();
var model = new PagedViewModel<IApiContentResponse>
{
Total = pagedResult.Total,
Items = apiContentItems
};
return await Task.FromResult(Ok(model));
}
}
@@ -1,49 +0,0 @@
using System.Text.Json;
using System.Text.Json.Serialization;
using Microsoft.Extensions.DependencyInjection;
using Umbraco.Cms.Api.Common.DependencyInjection;
using Umbraco.Cms.Api.Delivery.Accessors;
using Umbraco.Cms.Api.Delivery.Configuration;
using Umbraco.Cms.Api.Delivery.Json;
using Umbraco.Cms.Api.Delivery.Rendering;
using Umbraco.Cms.Api.Delivery.Services;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.DeliveryApi;
using Umbraco.Cms.Core.DependencyInjection;
namespace Umbraco.Extensions;
public static class UmbracoBuilderExtensions
{
public static IUmbracoBuilder AddDeliveryApi(this IUmbracoBuilder builder)
{
builder.Services.AddScoped<IRequestStartItemProvider, RequestStartItemProvider>();
builder.Services.AddScoped<IOutputExpansionStrategy, RequestContextOutputExpansionStrategy>();
builder.Services.AddSingleton<IRequestCultureService, RequestCultureService>();
builder.Services.AddSingleton<IRequestRoutingService, RequestRoutingService>();
builder.Services.AddSingleton<IRequestRedirectService, RequestRedirectService>();
builder.Services.AddSingleton<IRequestPreviewService, RequestPreviewService>();
builder.Services.AddSingleton<IOutputExpansionStrategyAccessor, RequestContextOutputExpansionStrategyAccessor>();
builder.Services.AddSingleton<IRequestStartItemProviderAccessor, RequestContextRequestStartItemProviderAccessor>();
builder.Services.AddSingleton<IApiAccessService, ApiAccessService>();
builder.Services.AddSingleton<IApiContentQueryService, ApiContentQueryService>();
builder.Services.AddSingleton<IApiContentQueryProvider, ApiContentQueryProvider>();
builder.Services.ConfigureOptions<ConfigureUmbracoDeliveryApiSwaggerGenOptions>();
builder.AddUmbracoApiOpenApiUI();
builder
.Services
.AddControllers()
.AddJsonOptions(Constants.JsonOptionsNames.DeliveryApi, options =>
{
// all Delivery API specific JSON options go here
options.JsonSerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.CamelCase;
options.JsonSerializerOptions.TypeInfoResolver = new DeliveryApiJsonTypeResolver();
options.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter());
});
return builder;
}
}
@@ -1,43 +0,0 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
using Umbraco.Cms.Core.DeliveryApi;
namespace Umbraco.Cms.Api.Delivery.Filters;
internal sealed class DeliveryApiAccessAttribute : TypeFilterAttribute
{
public DeliveryApiAccessAttribute()
: base(typeof(DeliveryApiAccessFilter))
{
}
private class DeliveryApiAccessFilter : IActionFilter
{
private readonly IApiAccessService _apiAccessService;
private readonly IRequestPreviewService _requestPreviewService;
public DeliveryApiAccessFilter(IApiAccessService apiAccessService, IRequestPreviewService requestPreviewService)
{
_apiAccessService = apiAccessService;
_requestPreviewService = requestPreviewService;
}
public void OnActionExecuting(ActionExecutingContext context)
{
var hasAccess = _requestPreviewService.IsPreview()
? _apiAccessService.HasPreviewAccess()
: _apiAccessService.HasPublicAccess();
if (hasAccess)
{
return;
}
context.Result = new UnauthorizedResult();
}
public void OnActionExecuted(ActionExecutedContext context)
{
}
}
}
@@ -1,37 +0,0 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
using Umbraco.Cms.Core.DeliveryApi;
using Umbraco.Extensions;
namespace Umbraco.Cms.Api.Delivery.Filters;
internal sealed class LocalizeFromAcceptLanguageHeaderAttribute : TypeFilterAttribute
{
public LocalizeFromAcceptLanguageHeaderAttribute()
: base(typeof(LocalizeFromAcceptLanguageHeaderAttributeFilter))
{
}
private class LocalizeFromAcceptLanguageHeaderAttributeFilter : IActionFilter
{
private readonly IRequestCultureService _requestCultureService;
public LocalizeFromAcceptLanguageHeaderAttributeFilter(IRequestCultureService requestCultureService)
=> _requestCultureService = requestCultureService;
public void OnActionExecuting(ActionExecutingContext context)
{
var requestedCulture = _requestCultureService.GetRequestedCulture();
if (requestedCulture.IsNullOrWhiteSpace())
{
return;
}
_requestCultureService.SetRequestCulture(requestedCulture);
}
public void OnActionExecuted(ActionExecutedContext context)
{
}
}
}
@@ -1,217 +0,0 @@
using Microsoft.OpenApi.Any;
using Microsoft.OpenApi.Models;
using Swashbuckle.AspNetCore.SwaggerGen;
using Umbraco.Cms.Api.Delivery.Configuration;
using Umbraco.Extensions;
namespace Umbraco.Cms.Api.Delivery.Filters;
public class SwaggerDocumentationFilter : IOperationFilter, IParameterFilter
{
public void Apply(OpenApiOperation operation, OperationFilterContext context)
{
if (context.MethodInfo.HasMapToApiAttribute(DeliveryApiConfiguration.ApiName) == false)
{
return;
}
operation.Parameters ??= new List<OpenApiParameter>();
operation.Parameters.Add(new OpenApiParameter
{
Name = "expand",
In = ParameterLocation.Query,
Required = false,
Description = QueryParameterDescription("Defines the properties that should be expanded in the response"),
Schema = new OpenApiSchema { Type = "string" },
Examples = new Dictionary<string, OpenApiExample>
{
{ "Expand none", new OpenApiExample { Value = new OpenApiString("") } },
{ "Expand all", new OpenApiExample { Value = new OpenApiString("all") } },
{
"Expand specific property",
new OpenApiExample { Value = new OpenApiString("property:alias1") }
},
{
"Expand specific properties",
new OpenApiExample { Value = new OpenApiString("property:alias1,alias2") }
}
}
});
operation.Parameters.Add(new OpenApiParameter
{
Name = "Accept-Language",
In = ParameterLocation.Header,
Required = false,
Description = "Defines the language to return. Use this when querying language variant content items.",
Schema = new OpenApiSchema { Type = "string" },
Examples = new Dictionary<string, OpenApiExample>
{
{ "Default", new OpenApiExample { Value = new OpenApiString("") } },
{ "English culture", new OpenApiExample { Value = new OpenApiString("en-us") } }
}
});
operation.Parameters.Add(new OpenApiParameter
{
Name = "Api-Key",
In = ParameterLocation.Header,
Required = false,
Description = "API key specified through configuration to authorize access to the API.",
Schema = new OpenApiSchema { Type = "string" }
});
operation.Parameters.Add(new OpenApiParameter
{
Name = "Preview",
In = ParameterLocation.Header,
Required = false,
Description = "Whether to request draft content.",
Schema = new OpenApiSchema { Type = "boolean" }
});
operation.Parameters.Add(new OpenApiParameter
{
Name = "Start-Item",
In = ParameterLocation.Header,
Required = false,
Description = "URL segment or GUID of a root content item.",
Schema = new OpenApiSchema { Type = "string" }
});
}
public void Apply(OpenApiParameter parameter, ParameterFilterContext context)
{
if (context.DocumentName != DeliveryApiConfiguration.ApiName)
{
return;
}
switch (parameter.Name)
{
case "fetch":
AddQueryParameterDocumentation(parameter, FetchQueryParameterExamples(), "Specifies the content items to fetch");
break;
case "filter":
AddQueryParameterDocumentation(parameter, FilterQueryParameterExamples(), "Defines how to filter the fetched content items");
break;
case "sort":
AddQueryParameterDocumentation(parameter, SortQueryParameterExamples(), "Defines how to sort the found content items");
break;
case "skip":
parameter.Description = PaginationDescription(true);
break;
case "take":
parameter.Description = PaginationDescription(false);
break;
default:
return;
}
}
private string QueryParameterDescription(string description)
=> $"{description}. Refer to [the documentation]({DeliveryApiConfiguration.ApiDocumentationArticleLink}#query-parameters) for more details on this.";
private string PaginationDescription(bool skip) => $"Specifies the number of found content items to {(skip ? "skip" : "take")}. Use this to control pagination of the response.";
private void AddQueryParameterDocumentation(OpenApiParameter parameter, Dictionary<string, OpenApiExample> examples, string description)
{
parameter.Description = QueryParameterDescription(description);
parameter.Examples = examples;
}
private Dictionary<string, OpenApiExample> FetchQueryParameterExamples() =>
new()
{
{ "Select all", new OpenApiExample { Value = new OpenApiString("") } },
{
"Select all ancestors of a node by id",
new OpenApiExample { Value = new OpenApiString("ancestors:id") }
},
{
"Select all ancestors of a node by path",
new OpenApiExample { Value = new OpenApiString("ancestors:path") }
},
{
"Select all children of a node by id",
new OpenApiExample { Value = new OpenApiString("children:id") }
},
{
"Select all children of a node by path",
new OpenApiExample { Value = new OpenApiString("children:path") }
},
{
"Select all descendants of a node by id",
new OpenApiExample { Value = new OpenApiString("descendants:id") }
},
{
"Select all descendants of a node by path",
new OpenApiExample { Value = new OpenApiString("descendants:path") }
}
};
private Dictionary<string, OpenApiExample> FilterQueryParameterExamples() =>
new()
{
{ "Default filter", new OpenApiExample { Value = new OpenApiString("") } },
{
"Filter by content type",
new OpenApiExample { Value = new OpenApiArray { new OpenApiString("contentType:alias1") } }
},
{
"Filter by name",
new OpenApiExample { Value = new OpenApiArray { new OpenApiString("name:nodeName") } }
}
};
private Dictionary<string, OpenApiExample> SortQueryParameterExamples() =>
new()
{
{ "Default sort", new OpenApiExample { Value = new OpenApiString("") } },
{
"Sort by create date",
new OpenApiExample
{
Value = new OpenApiArray
{
new OpenApiString("createDate:asc"), new OpenApiString("createDate:desc")
}
}
},
{
"Sort by level",
new OpenApiExample
{
Value = new OpenApiArray { new OpenApiString("level:asc"), new OpenApiString("level:desc") }
}
},
{
"Sort by name",
new OpenApiExample
{
Value = new OpenApiArray { new OpenApiString("name:asc"), new OpenApiString("name:desc") }
}
},
{
"Sort by sort order",
new OpenApiExample
{
Value = new OpenApiArray
{
new OpenApiString("sortOrder:asc"), new OpenApiString("sortOrder:desc")
}
}
},
{
"Sort by update date",
new OpenApiExample
{
Value = new OpenApiArray
{
new OpenApiString("updateDate:asc"), new OpenApiString("updateDate:desc")
}
}
}
};
}
@@ -1,42 +0,0 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
using Umbraco.Cms.Core.DeliveryApi;
using Umbraco.Cms.Core.Models.PublishedContent;
namespace Umbraco.Cms.Api.Delivery.Filters;
internal sealed class ValidateStartItemAttribute : TypeFilterAttribute
{
public ValidateStartItemAttribute()
: base(typeof(ValidateStartItemFilter))
{
}
private class ValidateStartItemFilter : IActionFilter
{
private readonly IRequestStartItemProviderAccessor _requestStartItemProviderAccessor;
public ValidateStartItemFilter(IRequestStartItemProviderAccessor requestStartItemProviderAccessor)
=> _requestStartItemProviderAccessor = requestStartItemProviderAccessor;
public void OnActionExecuting(ActionExecutingContext context)
{
if (_requestStartItemProviderAccessor.TryGetValue(out IRequestStartItemProvider? requestStartItemProvider) is false
|| requestStartItemProvider.RequestedStartItem() is null)
{
return;
}
IPublishedContent? startItem = requestStartItemProvider.GetStartItem();
if (startItem is null)
{
context.Result = new NotFoundObjectResult("The Start-Item could not be found");
}
}
public void OnActionExecuted(ActionExecutedContext context)
{
}
}
}
@@ -1,15 +0,0 @@
using Umbraco.Cms.Core.DeliveryApi;
using Umbraco.Cms.Core.Models;
namespace Umbraco.Cms.Api.Delivery.Indexing.Filters;
public sealed class ContentTypeFilterIndexer : IContentIndexHandler
{
internal const string FieldName = "contentType";
public IEnumerable<IndexFieldValue> GetFieldValues(IContent content, string? culture)
=> new[] { new IndexFieldValue { FieldName = FieldName, Values = new object[] { content.ContentType.Alias } } };
public IEnumerable<IndexField> GetFields()
=> new[] { new IndexField { FieldName = FieldName, FieldType = FieldType.StringRaw, VariesByCulture = false } };
}
@@ -1,15 +0,0 @@
using Umbraco.Cms.Core.DeliveryApi;
using Umbraco.Cms.Core.Models;
namespace Umbraco.Cms.Api.Delivery.Indexing.Filters;
public sealed class NameFilterIndexer : IContentIndexHandler
{
internal const string FieldName = "name";
public IEnumerable<IndexFieldValue> GetFieldValues(IContent content, string? culture)
=> new[] { new IndexFieldValue { FieldName = FieldName, Values = new object[] { content.GetCultureName(culture) ?? string.Empty } } };
public IEnumerable<IndexField> GetFields()
=> new[] { new IndexField { FieldName = FieldName, FieldType = FieldType.StringAnalyzed, VariesByCulture = true } };
}
@@ -1,17 +0,0 @@
using Umbraco.Cms.Core.DeliveryApi;
using Umbraco.Cms.Core.Models;
namespace Umbraco.Cms.Api.Delivery.Indexing.Selectors;
public sealed class AncestorsSelectorIndexer : IContentIndexHandler
{
// NOTE: "id" is a reserved field name
internal const string FieldName = "itemId";
public IEnumerable<IndexFieldValue> GetFieldValues(IContent content, string? culture)
=> new[] { new IndexFieldValue { FieldName = FieldName, Values = new object[] { content.Key } } };
public IEnumerable<IndexField> GetFields()
=> new[] { new IndexField { FieldName = FieldName, FieldType = FieldType.StringRaw, VariesByCulture = false } };
}
@@ -1,35 +0,0 @@
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.DeliveryApi;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Services;
namespace Umbraco.Cms.Api.Delivery.Indexing.Selectors;
public sealed class ChildrenSelectorIndexer : IContentIndexHandler
{
private readonly IEntityService _entityService;
public ChildrenSelectorIndexer(IEntityService entityService)
=> _entityService = entityService;
internal const string FieldName = "parentId";
public IEnumerable<IndexFieldValue> GetFieldValues(IContent content, string? culture)
{
Guid parentKey = Guid.Empty;
if (content.ParentId > 0)
{
Attempt<Guid> getKeyAttempt = _entityService.GetKey(content.ParentId, UmbracoObjectTypes.Document);
parentKey = getKeyAttempt.Success ? getKeyAttempt.Result : parentKey;
}
yield return new IndexFieldValue
{
FieldName = FieldName,
Values = new object[] { parentKey }
};
}
public IEnumerable<IndexField> GetFields()
=> new[] { new IndexField { FieldName = FieldName, FieldType = FieldType.StringRaw, VariesByCulture = false } };
}
@@ -1,36 +0,0 @@
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.DeliveryApi;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Services;
using Umbraco.Extensions;
namespace Umbraco.Cms.Api.Delivery.Indexing.Selectors;
public sealed class DescendantsSelectorIndexer : IContentIndexHandler
{
internal const string FieldName = "ancestorIds";
private readonly IEntityService _entityService;
public DescendantsSelectorIndexer(IEntityService entityService)
=> _entityService = entityService;
public IEnumerable<IndexFieldValue> GetFieldValues(IContent content, string? culture)
{
var ancestorKeys = content.GetAncestorIds()?.Select(id =>
{
Attempt<Guid> getKeyAttempt = _entityService.GetKey(id, UmbracoObjectTypes.Document);
return getKeyAttempt.Success ? getKeyAttempt.Result : (object)Guid.Empty;
}).ToArray() ?? Array.Empty<object>();
yield return new IndexFieldValue
{
FieldName = FieldName,
Values = ancestorKeys
};
}
public IEnumerable<IndexField> GetFields()
=> new[] { new IndexField { FieldName = FieldName, FieldType = FieldType.StringRaw, VariesByCulture = false } };
}
@@ -1,15 +0,0 @@
using Umbraco.Cms.Core.DeliveryApi;
using Umbraco.Cms.Core.Models;
namespace Umbraco.Cms.Api.Delivery.Indexing.Sorts;
public sealed class CreateDateSortIndexer : IContentIndexHandler
{
internal const string FieldName = "createDate";
public IEnumerable<IndexFieldValue> GetFieldValues(IContent content, string? culture)
=> new[] { new IndexFieldValue { FieldName = FieldName, Values = new object[] { content.CreateDate } } };
public IEnumerable<IndexField> GetFields()
=> new[] { new IndexField { FieldName = FieldName, FieldType = FieldType.Date, VariesByCulture = false } };
}
@@ -1,15 +0,0 @@
using Umbraco.Cms.Core.DeliveryApi;
using Umbraco.Cms.Core.Models;
namespace Umbraco.Cms.Api.Delivery.Indexing.Sorts;
public sealed class LevelSortIndexer : IContentIndexHandler
{
internal const string FieldName = "level";
public IEnumerable<IndexFieldValue> GetFieldValues(IContent content, string? culture)
=> new[] { new IndexFieldValue { FieldName = FieldName, Values = new object[] { content.Level } } };
public IEnumerable<IndexField> GetFields()
=> new[] { new IndexField { FieldName = FieldName, FieldType = FieldType.Number, VariesByCulture = false } };
}
@@ -1,15 +0,0 @@
using Umbraco.Cms.Core.DeliveryApi;
using Umbraco.Cms.Core.Models;
namespace Umbraco.Cms.Api.Delivery.Indexing.Sorts;
public sealed class NameSortIndexer : IContentIndexHandler
{
internal const string FieldName = "sortName";
public IEnumerable<IndexFieldValue> GetFieldValues(IContent content, string? culture)
=> new[] { new IndexFieldValue { FieldName = FieldName, Values = new object[] { content.GetCultureName(culture) ?? string.Empty } } };
public IEnumerable<IndexField> GetFields()
=> new[] { new IndexField { FieldName = FieldName, FieldType = FieldType.StringSortable, VariesByCulture = true } };
}
@@ -1,15 +0,0 @@
using Umbraco.Cms.Core.DeliveryApi;
using Umbraco.Cms.Core.Models;
namespace Umbraco.Cms.Api.Delivery.Indexing.Sorts;
public sealed class SortOrderSortIndexer : IContentIndexHandler
{
internal const string FieldName = "sortOrder";
public IEnumerable<IndexFieldValue> GetFieldValues(IContent content, string? culture)
=> new[] { new IndexFieldValue { FieldName = FieldName, Values = new object[] { content.SortOrder } } };
public IEnumerable<IndexField> GetFields()
=> new[] { new IndexField { FieldName = FieldName, FieldType = FieldType.Number, VariesByCulture = false } };
}
@@ -1,22 +0,0 @@
using Umbraco.Cms.Core.DeliveryApi;
using Umbraco.Cms.Core.Models;
namespace Umbraco.Cms.Api.Delivery.Indexing.Sorts;
public sealed class UpdateDateSortIndexer : IContentIndexHandler
{
internal const string FieldName = "updateDate";
public IEnumerable<IndexFieldValue> GetFieldValues(IContent content, string? culture)
=> new[]
{
new IndexFieldValue
{
FieldName = FieldName,
Values = new object[] { (culture is not null ? content.GetUpdateDate(culture) : null) ?? content.UpdateDate }
}
};
public IEnumerable<IndexField> GetFields()
=> new[] { new IndexField { FieldName = FieldName, FieldType = FieldType.Date, VariesByCulture = true } };
}
@@ -1,43 +0,0 @@
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Text.Json.Serialization.Metadata;
using Umbraco.Cms.Core.Models.DeliveryApi;
namespace Umbraco.Cms.Api.Delivery.Json;
// see https://learn.microsoft.com/en-us/dotnet/standard/serialization/system-text-json/polymorphism?pivots=dotnet-7-0
public class DeliveryApiJsonTypeResolver : DefaultJsonTypeInfoResolver
{
public override JsonTypeInfo GetTypeInfo(Type type, JsonSerializerOptions options)
{
JsonTypeInfo jsonTypeInfo = base.GetTypeInfo(type, options);
if (jsonTypeInfo.Type == typeof(IApiContent))
{
ConfigureJsonPolymorphismOptions(jsonTypeInfo, typeof(ApiContent));
}
else if (jsonTypeInfo.Type == typeof(IApiContentResponse))
{
ConfigureJsonPolymorphismOptions(jsonTypeInfo, typeof(ApiContentResponse));
}
else if (jsonTypeInfo.Type == typeof(IRichTextElement))
{
ConfigureJsonPolymorphismOptions(jsonTypeInfo, typeof(RichTextGenericElement), typeof(RichTextTextElement));
}
return jsonTypeInfo;
}
private void ConfigureJsonPolymorphismOptions(JsonTypeInfo jsonTypeInfo, params Type[] derivedTypes)
{
jsonTypeInfo.PolymorphismOptions = new JsonPolymorphismOptions
{
UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FailSerialization,
};
foreach (Type derivedType in derivedTypes)
{
jsonTypeInfo.PolymorphismOptions.DerivedTypes.Add(new JsonDerivedType(derivedType));
}
}
}
@@ -1,31 +0,0 @@
using Umbraco.Cms.Api.Delivery.Indexing.Filters;
using Umbraco.Cms.Core.DeliveryApi;
using Umbraco.Extensions;
namespace Umbraco.Cms.Api.Delivery.Querying.Filters;
public sealed class ContentTypeFilter : IFilterHandler
{
private const string ContentTypeSpecifier = "contentType:";
/// <inheritdoc />
public bool CanHandle(string query)
=> query.StartsWith(ContentTypeSpecifier, StringComparison.OrdinalIgnoreCase);
/// <inheritdoc/>
public FilterOption BuildFilterOption(string filter)
{
var alias = filter.Substring(ContentTypeSpecifier.Length);
return new FilterOption
{
FieldName = ContentTypeFilterIndexer.FieldName,
Values = alias.IsNullOrWhiteSpace() == false
? new[] { alias.TrimStart('!') }
: Array.Empty<string>(),
Operator = alias.StartsWith('!')
? FilterOperation.IsNot
: FilterOperation.Is
};
}
}
@@ -1,31 +0,0 @@
using Umbraco.Cms.Api.Delivery.Indexing.Filters;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.DeliveryApi;
namespace Umbraco.Cms.Api.Delivery.Querying.Filters;
public sealed class NameFilter : IFilterHandler
{
private const string NameSpecifier = "name:";
/// <inheritdoc />
public bool CanHandle(string query)
=> query.StartsWith(NameSpecifier, StringComparison.OrdinalIgnoreCase);
/// <inheritdoc/>
public FilterOption BuildFilterOption(string filter)
{
var value = filter.Substring(NameSpecifier.Length);
var negate = value.StartsWith('!');
var values = value.TrimStart('!').Split(Constants.CharArrays.Comma);
return new FilterOption
{
FieldName = NameFilterIndexer.FieldName,
Values = values,
Operator = negate
? FilterOperation.DoesNotContain
: FilterOperation.Contains
};
}
}
@@ -1,40 +0,0 @@
using Umbraco.Cms.Core.DeliveryApi;
using Umbraco.Cms.Core.Models.PublishedContent;
using Umbraco.Cms.Core.PublishedCache;
using Umbraco.Extensions;
namespace Umbraco.Cms.Api.Delivery.Querying;
public abstract class QueryOptionBase
{
private readonly IPublishedSnapshotAccessor _publishedSnapshotAccessor;
private readonly IRequestRoutingService _requestRoutingService;
public QueryOptionBase(IPublishedSnapshotAccessor publishedSnapshotAccessor,
IRequestRoutingService requestRoutingService)
{
_publishedSnapshotAccessor = publishedSnapshotAccessor;
_requestRoutingService = requestRoutingService;
}
protected Guid? GetGuidFromQuery(string queryStringValue)
{
if (queryStringValue.IsNullOrWhiteSpace())
{
return null;
}
if (Guid.TryParse(queryStringValue, out Guid id))
{
return id;
}
IPublishedSnapshot publishedSnapshot = _publishedSnapshotAccessor.GetRequiredPublishedSnapshot();
// Check if the passed value is a path of a content item
var contentRoute = _requestRoutingService.GetContentRoute(queryStringValue);
IPublishedContent? contentItem = publishedSnapshot.Content?.GetByRoute(contentRoute);
return contentItem?.Key;
}
}
@@ -1,53 +0,0 @@
using Umbraco.Cms.Api.Delivery.Indexing.Selectors;
using Umbraco.Cms.Core.DeliveryApi;
using Umbraco.Cms.Core.Models.PublishedContent;
using Umbraco.Cms.Core.PublishedCache;
using Umbraco.Extensions;
namespace Umbraco.Cms.Api.Delivery.Querying.Selectors;
public sealed class AncestorsSelector : QueryOptionBase, ISelectorHandler
{
private const string AncestorsSpecifier = "ancestors:";
private readonly IPublishedSnapshotAccessor _publishedSnapshotAccessor;
public AncestorsSelector(IPublishedSnapshotAccessor publishedSnapshotAccessor, IRequestRoutingService requestRoutingService)
: base(publishedSnapshotAccessor, requestRoutingService) =>
_publishedSnapshotAccessor = publishedSnapshotAccessor;
/// <inheritdoc />
public bool CanHandle(string query)
=> query.StartsWith(AncestorsSpecifier, StringComparison.OrdinalIgnoreCase);
/// <inheritdoc/>
public SelectorOption BuildSelectorOption(string selector)
{
var fieldValue = selector[AncestorsSpecifier.Length..];
Guid? id = GetGuidFromQuery(fieldValue);
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,
// it means that CanHandle() returned true, meaning that this Selector should be able to handle the selector value
return new SelectorOption
{
FieldName = AncestorsSelectorIndexer.FieldName,
Values = Array.Empty<string>()
};
}
IPublishedSnapshot publishedSnapshot = _publishedSnapshotAccessor.GetRequiredPublishedSnapshot();
IPublishedContent contentItem = publishedSnapshot.Content?.GetById((Guid)id)
?? throw new InvalidOperationException("Could not obtain the content cache");
var ancestorKeys = contentItem.Ancestors().Select(a => a.Key.ToString("D")).ToArray();
return new SelectorOption
{
FieldName = AncestorsSelectorIndexer.FieldName,
Values = ancestorKeys
};
}
}
@@ -1,35 +0,0 @@
using Umbraco.Cms.Api.Delivery.Indexing.Selectors;
using Umbraco.Cms.Core.DeliveryApi;
using Umbraco.Cms.Core.PublishedCache;
using Umbraco.Extensions;
namespace Umbraco.Cms.Api.Delivery.Querying.Selectors;
public sealed class ChildrenSelector : QueryOptionBase, ISelectorHandler
{
private const string ChildrenSpecifier = "children:";
public ChildrenSelector(IPublishedSnapshotAccessor publishedSnapshotAccessor, IRequestRoutingService requestRoutingService)
: base(publishedSnapshotAccessor, requestRoutingService)
{
}
/// <inheritdoc />
public bool CanHandle(string query)
=> query.StartsWith(ChildrenSpecifier, StringComparison.OrdinalIgnoreCase);
/// <inheritdoc/>
public SelectorOption BuildSelectorOption(string selector)
{
var fieldValue = selector[ChildrenSpecifier.Length..];
var id = GetGuidFromQuery(fieldValue)?.ToString("D");
return new SelectorOption
{
FieldName = ChildrenSelectorIndexer.FieldName,
Values = id.IsNullOrWhiteSpace() == false
? new[] { id }
: Array.Empty<string>()
};
}
}
@@ -1,35 +0,0 @@
using Umbraco.Cms.Api.Delivery.Indexing.Selectors;
using Umbraco.Cms.Core.DeliveryApi;
using Umbraco.Cms.Core.PublishedCache;
using Umbraco.Extensions;
namespace Umbraco.Cms.Api.Delivery.Querying.Selectors;
public sealed class DescendantsSelector : QueryOptionBase, ISelectorHandler
{
private const string DescendantsSpecifier = "descendants:";
public DescendantsSelector(IPublishedSnapshotAccessor publishedSnapshotAccessor, IRequestRoutingService requestRoutingService)
: base(publishedSnapshotAccessor, requestRoutingService)
{
}
/// <inheritdoc />
public bool CanHandle(string query)
=> query.StartsWith(DescendantsSpecifier, StringComparison.OrdinalIgnoreCase);
/// <inheritdoc/>
public SelectorOption BuildSelectorOption(string selector)
{
var fieldValue = selector[DescendantsSpecifier.Length..];
var id = GetGuidFromQuery(fieldValue)?.ToString("D");
return new SelectorOption
{
FieldName = DescendantsSelectorIndexer.FieldName,
Values = id.IsNullOrWhiteSpace() == false
? new[] { id }
: Array.Empty<string>()
};
}
}
@@ -1,26 +0,0 @@
using Umbraco.Cms.Api.Delivery.Indexing.Sorts;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.DeliveryApi;
namespace Umbraco.Cms.Api.Delivery.Querying.Sorts;
public sealed class CreateDateSort : ISortHandler
{
private const string SortOptionSpecifier = "createDate:";
/// <inheritdoc />
public bool CanHandle(string query)
=> query.StartsWith(SortOptionSpecifier, StringComparison.OrdinalIgnoreCase);
/// <inheritdoc/>
public SortOption BuildSortOption(string sort)
{
var sortDirection = sort.Substring(SortOptionSpecifier.Length);
return new SortOption
{
FieldName = CreateDateSortIndexer.FieldName,
Direction = sortDirection.StartsWith("asc") ? Direction.Ascending : Direction.Descending
};
}
}
@@ -1,26 +0,0 @@
using Umbraco.Cms.Api.Delivery.Indexing.Sorts;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.DeliveryApi;
namespace Umbraco.Cms.Api.Delivery.Querying.Sorts;
public sealed class LevelSort : ISortHandler
{
private const string SortOptionSpecifier = "level:";
/// <inheritdoc />
public bool CanHandle(string query)
=> query.StartsWith(SortOptionSpecifier, StringComparison.OrdinalIgnoreCase);
/// <inheritdoc/>
public SortOption BuildSortOption(string sort)
{
var sortDirection = sort.Substring(SortOptionSpecifier.Length);
return new SortOption
{
FieldName = LevelSortIndexer.FieldName,
Direction = sortDirection.StartsWith("asc") ? Direction.Ascending : Direction.Descending
};
}
}
@@ -1,26 +0,0 @@
using Umbraco.Cms.Api.Delivery.Indexing.Sorts;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.DeliveryApi;
namespace Umbraco.Cms.Api.Delivery.Querying.Sorts;
public sealed class NameSort : ISortHandler
{
private const string SortOptionSpecifier = "name:";
/// <inheritdoc />
public bool CanHandle(string query)
=> query.StartsWith(SortOptionSpecifier, StringComparison.OrdinalIgnoreCase);
/// <inheritdoc/>
public SortOption BuildSortOption(string sort)
{
var sortDirection = sort.Substring(SortOptionSpecifier.Length);
return new SortOption
{
FieldName = NameSortIndexer.FieldName,
Direction = sortDirection.StartsWith("asc") ? Direction.Ascending : Direction.Descending
};
}
}
@@ -1,26 +0,0 @@
using Umbraco.Cms.Api.Delivery.Indexing.Sorts;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.DeliveryApi;
namespace Umbraco.Cms.Api.Delivery.Querying.Sorts;
public sealed class SortOrderSort : ISortHandler
{
private const string SortOptionSpecifier = "sortOrder:";
/// <inheritdoc />
public bool CanHandle(string query)
=> query.StartsWith(SortOptionSpecifier, StringComparison.OrdinalIgnoreCase);
/// <inheritdoc/>
public SortOption BuildSortOption(string sort)
{
var sortDirection = sort.Substring(SortOptionSpecifier.Length);
return new SortOption
{
FieldName = SortOrderSortIndexer.FieldName,
Direction = sortDirection.StartsWith("asc") ? Direction.Ascending : Direction.Descending
};
}
}
@@ -1,26 +0,0 @@
using Umbraco.Cms.Api.Delivery.Indexing.Sorts;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.DeliveryApi;
namespace Umbraco.Cms.Api.Delivery.Querying.Sorts;
public sealed class UpdateDateSort : ISortHandler
{
private const string SortOptionSpecifier = "updateDate:";
/// <inheritdoc />
public bool CanHandle(string query)
=> query.StartsWith(SortOptionSpecifier, StringComparison.OrdinalIgnoreCase);
/// <inheritdoc/>
public SortOption BuildSortOption(string sort)
{
var sortDirection = sort.Substring(SortOptionSpecifier.Length);
return new SortOption
{
FieldName = UpdateDateSortIndexer.FieldName,
Direction = sortDirection.StartsWith("asc") ? Direction.Ascending : Direction.Descending
};
}
}
@@ -1,123 +0,0 @@
using Microsoft.AspNetCore.Http;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.DeliveryApi;
using Umbraco.Cms.Core.Models.PublishedContent;
using Umbraco.Extensions;
namespace Umbraco.Cms.Api.Delivery.Rendering;
internal sealed class RequestContextOutputExpansionStrategy : IOutputExpansionStrategy
{
private readonly IApiPropertyRenderer _propertyRenderer;
private readonly bool _expandAll;
private readonly string[] _expandAliases;
private ExpansionState _state;
public RequestContextOutputExpansionStrategy(IHttpContextAccessor httpContextAccessor, IApiPropertyRenderer propertyRenderer)
{
_propertyRenderer = propertyRenderer;
(bool ExpandAll, string[] ExpanedAliases) initialState = InitialRequestState(httpContextAccessor);
_expandAll = initialState.ExpandAll;
_expandAliases = initialState.ExpanedAliases;
_state = ExpansionState.Initial;
}
public IDictionary<string, object?> MapElementProperties(IPublishedElement element)
=> element.Properties.ToDictionary(
p => p.Alias,
p => GetPropertyValue(p, _state == ExpansionState.Expanding));
public IDictionary<string, object?> MapContentProperties(IPublishedContent content)
=> content.ItemType == PublishedItemType.Content
? MapProperties(content.Properties)
: throw new ArgumentException($"Invalid item type. This method can only be used with item type {nameof(PublishedItemType.Content)}, got: {content.ItemType}");
public IDictionary<string, object?> MapMediaProperties(IPublishedContent media, bool skipUmbracoProperties = true)
{
if (media.ItemType != PublishedItemType.Media)
{
throw new ArgumentException($"Invalid item type. This method can only be used with item type {PublishedItemType.Media}, got: {media.ItemType}");
}
IPublishedProperty[] properties = media
.Properties
.Where(p => skipUmbracoProperties is false || p.Alias.StartsWith("umbraco") is false)
.ToArray();
return properties.Any()
? MapProperties(properties)
: new Dictionary<string, object?>();
}
private IDictionary<string, object?> MapProperties(IEnumerable<IPublishedProperty> properties)
{
// in the initial state, content properties should always be rendered (expanded if the requests dictates it).
// this corresponds to the root level of a content item, i.e. when the initial content rendering starts.
if (_state == ExpansionState.Initial)
{
// update state to pending so we don't end up here the next time around
_state = ExpansionState.Pending;
var rendered = properties.ToDictionary(
property => property.Alias,
property =>
{
// update state to expanding if the property should be expanded (needed for nested elements)
if (_expandAll || _expandAliases.Contains(property.Alias))
{
_state = ExpansionState.Expanding;
}
var value = GetPropertyValue(property, _state == ExpansionState.Expanding);
// always revert to pending after rendering the property value
_state = ExpansionState.Pending;
return value;
});
_state = ExpansionState.Initial;
return rendered;
}
// in an expanding state, properties should always be rendered as collapsed.
// this corresponds to properties of a content based property placed directly below a root level property that is being expanded
// (i.e. properties for picked content for an expanded content picker at root level).
if (_state == ExpansionState.Expanding)
{
_state = ExpansionState.Expanded;
var rendered = properties.ToDictionary(
property => property.Alias,
property => GetPropertyValue(property, false));
_state = ExpansionState.Expanding;
return rendered;
}
return new Dictionary<string, object?>();
}
private (bool ExpandAll, string[] ExpanedAliases) InitialRequestState(IHttpContextAccessor httpContextAccessor)
{
string? toExpand = httpContextAccessor.HttpContext?.Request.Query["expand"];
if (toExpand.IsNullOrWhiteSpace())
{
return new(false, Array.Empty<string>());
}
const string propertySpecifier = "property:";
return new(
toExpand == "all",
toExpand.StartsWith(propertySpecifier)
? toExpand.Substring(propertySpecifier.Length).Split(Constants.CharArrays.Comma)
: Array.Empty<string>());
}
private object? GetPropertyValue(IPublishedProperty property, bool expanding)
=> _propertyRenderer.GetPropertyValue(property, expanding);
private enum ExpansionState
{
Initial,
Pending,
Expanding,
Expanded
}
}
@@ -1,11 +0,0 @@
using Umbraco.Cms.Web.Common.Routing;
namespace Umbraco.Cms.Api.Delivery.Routing;
internal sealed class VersionedDeliveryApiRouteAttribute : BackOfficeRouteAttribute
{
public VersionedDeliveryApiRouteAttribute(string template)
: base($"delivery/api/v{{version:apiVersion}}/{template.TrimStart('/')}")
{
}
}
@@ -1,30 +0,0 @@
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Options;
using Umbraco.Cms.Core.Configuration.Models;
using Umbraco.Cms.Core.DeliveryApi;
using Umbraco.Extensions;
namespace Umbraco.Cms.Api.Delivery.Services;
internal sealed class ApiAccessService : RequestHeaderHandler, IApiAccessService
{
private DeliveryApiSettings _deliveryApiSettings;
public ApiAccessService(IHttpContextAccessor httpContextAccessor, IOptionsMonitor<DeliveryApiSettings> deliveryApiSettings)
: base(httpContextAccessor)
{
_deliveryApiSettings = deliveryApiSettings.CurrentValue;
deliveryApiSettings.OnChange(settings => _deliveryApiSettings = settings);
}
/// <inheritdoc />
public bool HasPublicAccess() => IfEnabled(() => _deliveryApiSettings.PublicAccess || HasValidApiKey());
/// <inheritdoc />
public bool HasPreviewAccess() => IfEnabled(HasValidApiKey);
private bool IfEnabled(Func<bool> condition) => _deliveryApiSettings.Enabled && condition();
private bool HasValidApiKey() => _deliveryApiSettings.ApiKey.IsNullOrWhiteSpace() == false
&& _deliveryApiSettings.ApiKey.Equals(GetHeaderValue("Api-Key"));
}
@@ -1,194 +0,0 @@
using Examine;
using Examine.Lucene.Providers;
using Examine.Lucene.Search;
using Examine.Search;
using Microsoft.Extensions.Logging;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.DeliveryApi;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Infrastructure.Examine;
using Umbraco.Extensions;
namespace Umbraco.Cms.Api.Delivery.Services;
/// <summary>
/// This is the Examine implementation of content querying for the Delivery API.
/// </summary>
internal sealed class ApiContentQueryProvider : IApiContentQueryProvider
{
private const string ItemIdFieldName = "itemId";
private readonly IExamineManager _examineManager;
private readonly ILogger<ApiContentQueryProvider> _logger;
private readonly string _fallbackGuidValue;
private readonly Dictionary<string, FieldType> _fieldTypes;
public ApiContentQueryProvider(
IExamineManager examineManager,
ContentIndexHandlerCollection indexHandlers,
ILogger<ApiContentQueryProvider> logger)
{
_examineManager = examineManager;
_logger = logger;
// A fallback value is needed for Examine queries in case we don't have a value - we can't pass null or empty string
// It is set to a random guid since this would be highly unlikely to yield any results
_fallbackGuidValue = Guid.NewGuid().ToString("D");
// build a look-up dictionary of field types by field name
_fieldTypes = indexHandlers
.SelectMany(handler => handler.GetFields())
.DistinctBy(field => field.FieldName)
.ToDictionary(field => field.FieldName, field => field.FieldType, StringComparer.InvariantCultureIgnoreCase);
}
public PagedModel<Guid> ExecuteQuery(SelectorOption selectorOption, IList<FilterOption> filterOptions, IList<SortOption> sortOptions, string culture, bool preview, int skip, int take)
{
if (!_examineManager.TryGetIndex(Constants.UmbracoIndexes.DeliveryApiContentIndexName, out IIndex? index))
{
_logger.LogError("Could not find the index {IndexName} when attempting to execute a query.", Constants.UmbracoIndexes.DeliveryApiContentIndexName);
return new PagedModel<Guid>();
}
IBooleanOperation queryOperation = BuildSelectorOperation(selectorOption, index, culture, preview);
ApplyFiltering(filterOptions, queryOperation);
ApplySorting(sortOptions, queryOperation);
ISearchResults? results = queryOperation
.SelectField(ItemIdFieldName)
.Execute(QueryOptions.SkipTake(skip, take));
if (results is null)
{
// The query yield no results
return new PagedModel<Guid>();
}
Guid[] items = results
.Where(r => r.Values.ContainsKey(ItemIdFieldName))
.Select(r => Guid.Parse(r.Values[ItemIdFieldName]))
.ToArray();
return new PagedModel<Guid>(results.TotalItemCount, items);
}
public SelectorOption AllContentSelectorOption() => new()
{
FieldName = UmbracoExamineFieldNames.CategoryFieldName, Values = new[] { "content" }
};
private IBooleanOperation BuildSelectorOperation(SelectorOption selectorOption, IIndex index, string culture, bool preview)
{
// Needed for enabling leading wildcards searches
BaseLuceneSearcher searcher = index.Searcher as BaseLuceneSearcher ?? throw new InvalidOperationException($"Index searcher must be of type {nameof(BaseLuceneSearcher)}.");
IQuery query = searcher.CreateQuery(
IndexTypes.Content,
BooleanOperation.And,
searcher.LuceneAnalyzer,
new LuceneSearchOptions { AllowLeadingWildcard = true });
IBooleanOperation selectorOperation = selectorOption.Values.Length == 1
? query.Field(selectorOption.FieldName, selectorOption.Values.First())
: query.GroupedOr(new[] { selectorOption.FieldName }, selectorOption.Values);
// Item culture must be either the requested culture or "none"
selectorOperation.And().GroupedOr(new[] { UmbracoExamineFieldNames.DeliveryApiContentIndex.Culture }, culture.ToLowerInvariant().IfNullOrWhiteSpace(_fallbackGuidValue), "none");
// when not fetching for preview, make sure the "published" field is "y"
if (preview is false)
{
selectorOperation.And().Field(UmbracoExamineFieldNames.DeliveryApiContentIndex.Published, "y");
}
return selectorOperation;
}
private void ApplyFiltering(IList<FilterOption> filterOptions, IBooleanOperation queryOperation)
{
void HandleExact(IQuery query, string fieldName, string[] values)
{
if (values.Length == 1)
{
query.Field(fieldName, values[0]);
}
else
{
query.GroupedOr(new[] { fieldName }, values);
}
}
void HandleContains(IQuery query, string fieldName, string[] values)
{
if (values.Length == 1)
{
// The trailing wildcard is added automatically
query.Field(fieldName, (IExamineValue)new ExamineValue(Examineness.ComplexWildcard, $"*{values[0]}"));
}
else
{
// The trailing wildcard is added automatically
IExamineValue[] examineValues = values
.Select(value => (IExamineValue)new ExamineValue(Examineness.ComplexWildcard, $"*{value}"))
.ToArray();
query.GroupedOr(new[] { fieldName }, examineValues);
}
}
foreach (FilterOption filterOption in filterOptions)
{
var values = filterOption.Values.Any()
? filterOption.Values
: new[] { _fallbackGuidValue };
switch (filterOption.Operator)
{
case FilterOperation.Is:
HandleExact(queryOperation.And(), filterOption.FieldName, values);
break;
case FilterOperation.IsNot:
HandleExact(queryOperation.Not(), filterOption.FieldName, values);
break;
case FilterOperation.Contains:
HandleContains(queryOperation.And(), filterOption.FieldName, values);
break;
case FilterOperation.DoesNotContain:
HandleContains(queryOperation.Not(), filterOption.FieldName, values);
break;
default:
continue;
}
}
}
private void ApplySorting(IList<SortOption> sortOptions, IOrdering ordering)
{
foreach (SortOption sort in sortOptions)
{
if (_fieldTypes.TryGetValue(sort.FieldName, out FieldType fieldType) is false)
{
_logger.LogWarning(
"Sort implementation for field name {FieldName} does not match an index handler implementation, cannot resolve field type.",
sort.FieldName);
continue;
}
SortType sortType = fieldType switch
{
FieldType.Number => SortType.Int,
FieldType.Date => SortType.Long,
FieldType.StringRaw => SortType.String,
FieldType.StringAnalyzed => SortType.String,
FieldType.StringSortable => SortType.String,
_ => throw new ArgumentOutOfRangeException(nameof(fieldType))
};
ordering = sort.Direction switch
{
Direction.Ascending => ordering.OrderBy(new SortableField(sort.FieldName, sortType)),
Direction.Descending => ordering.OrderByDescending(new SortableField(sort.FieldName, sortType)),
_ => ordering
};
}
}
}
@@ -1,119 +0,0 @@
using Umbraco.Cms.Api.Delivery.Indexing.Selectors;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.DeliveryApi;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.PublishedContent;
using Umbraco.Cms.Core.Services.OperationStatus;
using Umbraco.Extensions;
namespace Umbraco.Cms.Api.Delivery.Services;
internal sealed class ApiContentQueryService : IApiContentQueryService
{
private readonly IRequestStartItemProviderAccessor _requestStartItemProviderAccessor;
private readonly SelectorHandlerCollection _selectorHandlers;
private readonly FilterHandlerCollection _filterHandlers;
private readonly SortHandlerCollection _sortHandlers;
private readonly IVariationContextAccessor _variationContextAccessor;
private readonly IApiContentQueryProvider _apiContentQueryProvider;
private readonly IRequestPreviewService _requestPreviewService;
public ApiContentQueryService(
IRequestStartItemProviderAccessor requestStartItemProviderAccessor,
SelectorHandlerCollection selectorHandlers,
FilterHandlerCollection filterHandlers,
SortHandlerCollection sortHandlers,
IVariationContextAccessor variationContextAccessor,
IApiContentQueryProvider apiContentQueryProvider,
IRequestPreviewService requestPreviewService)
{
_requestStartItemProviderAccessor = requestStartItemProviderAccessor;
_selectorHandlers = selectorHandlers;
_filterHandlers = filterHandlers;
_sortHandlers = sortHandlers;
_variationContextAccessor = variationContextAccessor;
_apiContentQueryProvider = apiContentQueryProvider;
_requestPreviewService = requestPreviewService;
}
/// <inheritdoc/>
public Attempt<PagedModel<Guid>, ApiContentQueryOperationStatus> ExecuteQuery(string? fetch, IEnumerable<string> filters, IEnumerable<string> sorts, int skip, int take)
{
var emptyResult = new PagedModel<Guid>();
SelectorOption? selectorOption = GetSelectorOption(fetch);
if (selectorOption is null)
{
// If no Selector could be found, we return no results
return Attempt.FailWithStatus(ApiContentQueryOperationStatus.SelectorOptionNotFound, emptyResult);
}
var filterOptions = new List<FilterOption>();
foreach (var filter in filters.Where(filter => filter.IsNullOrWhiteSpace() is false))
{
FilterOption? filterOption = GetFilterOption(filter);
if (filterOption is null)
{
// If there is an invalid Filter option, we return no results
return Attempt.FailWithStatus(ApiContentQueryOperationStatus.FilterOptionNotFound, emptyResult);
}
filterOptions.Add(filterOption);
}
var sortOptions = new List<SortOption>();
foreach (var sort in sorts.Where(sort => sort.IsNullOrWhiteSpace() is false))
{
SortOption? sortOption = GetSortOption(sort);
if (sortOption is null)
{
// If there is an invalid Sort option, we return no results
return Attempt.FailWithStatus(ApiContentQueryOperationStatus.SortOptionNotFound, emptyResult);
}
sortOptions.Add(sortOption);
}
var culture = _variationContextAccessor.VariationContext?.Culture ?? string.Empty;
var isPreview = _requestPreviewService.IsPreview();
PagedModel<Guid> result = _apiContentQueryProvider.ExecuteQuery(selectorOption, filterOptions, sortOptions, culture, isPreview, skip, take);
return Attempt.SucceedWithStatus(ApiContentQueryOperationStatus.Success, result);
}
private SelectorOption? GetSelectorOption(string? fetch)
{
if (fetch is not null)
{
ISelectorHandler? selectorHandler = _selectorHandlers.FirstOrDefault(h => h.CanHandle(fetch));
return selectorHandler?.BuildSelectorOption(fetch);
}
if (_requestStartItemProviderAccessor.TryGetValue(out IRequestStartItemProvider? requestStartItemProvider))
{
IPublishedContent? startItem = requestStartItemProvider.GetStartItem();
if (startItem is not null)
{
// Reusing the boolean operation of the "Descendants" selector, as we want to get all the nodes from the given starting point
return new SelectorOption
{
FieldName = DescendantsSelectorIndexer.FieldName, Values = new[] { startItem.Key.ToString() }
};
}
}
return _apiContentQueryProvider.AllContentSelectorOption();
}
private FilterOption? GetFilterOption(string filter)
{
IFilterHandler? filterHandler = _filterHandlers.FirstOrDefault(h => h.CanHandle(filter));
return filterHandler?.BuildFilterOption(filter);
}
private SortOption? GetSortOption(string sort)
{
ISortHandler? sortHandler = _sortHandlers.FirstOrDefault(h => h.CanHandle(sort));
return sortHandler?.BuildSortOption(sort);
}
}
@@ -1,39 +0,0 @@
using System.Text.RegularExpressions;
using Microsoft.AspNetCore.Http;
using Microsoft.Net.Http.Headers;
using Umbraco.Cms.Core.DeliveryApi;
using Umbraco.Cms.Core.Models.PublishedContent;
namespace Umbraco.Cms.Api.Delivery.Services;
internal sealed partial class RequestCultureService : RequestHeaderHandler, IRequestCultureService
{
private readonly IVariationContextAccessor _variationContextAccessor;
public RequestCultureService(IHttpContextAccessor httpContextAccessor, IVariationContextAccessor variationContextAccessor)
: base(httpContextAccessor) =>
_variationContextAccessor = variationContextAccessor;
/// <inheritdoc />
public string? GetRequestedCulture()
{
var acceptLanguage = GetHeaderValue(HeaderNames.AcceptLanguage) ?? string.Empty;
return ValidLanguageHeaderRegex().IsMatch(acceptLanguage) ? acceptLanguage : null;
}
/// <inheritdoc />
public void SetRequestCulture(string culture)
{
if (_variationContextAccessor.VariationContext?.Culture == culture)
{
return;
}
_variationContextAccessor.VariationContext = new VariationContext(culture);
}
// at the time of writing we're introducing this to get rid of accept-language header values like "en-GB,en-US;q=0.9,en;q=0.8",
// so we don't want to be too restrictive in this regex - keep it simple for now.
[GeneratedRegex(@"^[\w-]*$")]
private static partial Regex ValidLanguageHeaderRegex();
}
@@ -1,18 +0,0 @@
using Microsoft.AspNetCore.Http;
namespace Umbraco.Cms.Api.Delivery.Services;
internal abstract class RequestHeaderHandler
{
private readonly IHttpContextAccessor _httpContextAccessor;
protected RequestHeaderHandler(IHttpContextAccessor httpContextAccessor) => _httpContextAccessor = httpContextAccessor;
protected string? GetHeaderValue(string headerName)
{
HttpContext httpContext = _httpContextAccessor.HttpContext ??
throw new InvalidOperationException("Could not obtain an HTTP context");
return httpContext.Request.Headers[headerName];
}
}
@@ -1,15 +0,0 @@
using Microsoft.AspNetCore.Http;
using Umbraco.Cms.Core.DeliveryApi;
namespace Umbraco.Cms.Api.Delivery.Services;
internal sealed class RequestPreviewService : RequestHeaderHandler, IRequestPreviewService
{
public RequestPreviewService(IHttpContextAccessor httpContextAccessor)
: base(httpContextAccessor)
{
}
/// <inheritdoc />
public bool IsPreview() => GetHeaderValue("Preview") == "true";
}
@@ -1,78 +0,0 @@
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Options;
using Umbraco.Cms.Core.Configuration.Models;
using Umbraco.Cms.Core.DeliveryApi;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.DeliveryApi;
using Umbraco.Cms.Core.Models.PublishedContent;
using Umbraco.Cms.Core.PublishedCache;
using Umbraco.Cms.Core.Routing;
using Umbraco.Cms.Core.Services;
using Umbraco.Extensions;
namespace Umbraco.Cms.Api.Delivery.Services;
internal sealed class RequestRedirectService : RoutingServiceBase, IRequestRedirectService
{
private readonly IRequestCultureService _requestCultureService;
private readonly IRedirectUrlService _redirectUrlService;
private readonly IApiPublishedContentCache _apiPublishedContentCache;
private readonly IApiContentRouteBuilder _apiContentRouteBuilder;
private readonly GlobalSettings _globalSettings;
public RequestRedirectService(
IPublishedSnapshotAccessor publishedSnapshotAccessor,
IHttpContextAccessor httpContextAccessor,
IRequestStartItemProviderAccessor requestStartItemProviderAccessor,
IRequestCultureService requestCultureService,
IRedirectUrlService redirectUrlService,
IApiPublishedContentCache apiPublishedContentCache,
IApiContentRouteBuilder apiContentRouteBuilder,
IOptions<GlobalSettings> globalSettings)
: base(publishedSnapshotAccessor, httpContextAccessor, requestStartItemProviderAccessor)
{
_requestCultureService = requestCultureService;
_redirectUrlService = redirectUrlService;
_apiPublishedContentCache = apiPublishedContentCache;
_apiContentRouteBuilder = apiContentRouteBuilder;
_globalSettings = globalSettings.Value;
}
public IApiContentRoute? GetRedirectRoute(string requestedPath)
{
requestedPath = requestedPath.EnsureStartsWith("/");
// 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)
{
IPublishedContent? startItem = GetStartItem();
if (startItem?.UrlSegment != null)
{
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)
{
requestedPath = GetContentRoute(domainAndUri, contentRoute);
culture ??= domainAndUri.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;
return content != null
? _apiContentRouteBuilder.Build(content)
: null;
}
}
@@ -1,61 +0,0 @@
using Microsoft.AspNetCore.Http;
using Umbraco.Cms.Core.DeliveryApi;
using Umbraco.Cms.Core.Models.PublishedContent;
using Umbraco.Cms.Core.PublishedCache;
using Umbraco.Cms.Core.Routing;
using Umbraco.Extensions;
namespace Umbraco.Cms.Api.Delivery.Services;
internal sealed class RequestRoutingService : RoutingServiceBase, IRequestRoutingService
{
private readonly IRequestCultureService _requestCultureService;
public RequestRoutingService(
IPublishedSnapshotAccessor publishedSnapshotAccessor,
IHttpContextAccessor httpContextAccessor,
IRequestStartItemProviderAccessor requestStartItemProviderAccessor,
IRequestCultureService requestCultureService)
: base(publishedSnapshotAccessor, httpContextAccessor, requestStartItemProviderAccessor) =>
_requestCultureService = requestCultureService;
/// <inheritdoc />
public string GetContentRoute(string requestedPath)
{
if (requestedPath.IsNullOrWhiteSpace())
{
return string.Empty;
}
requestedPath = requestedPath.EnsureStartsWith("/");
// do we have an explicit start item?
IPublishedContent? startItem = GetStartItem();
if (startItem != null)
{
// the content cache can resolve content by the route "{root ID}/{content path}", which is what we construct here
return $"{startItem.Id}{requestedPath}";
}
// construct the (assumed) absolute URL for the requested content, and use that
// to look for a domain configuration that would match the URL
Uri contentRoute = GetDefaultRequestUri(requestedPath);
DomainAndUri? domainAndUri = GetDomainAndUriForRoute(contentRoute);
if (domainAndUri == null)
{
// no start item was found and no domain could be resolved, we will return the requested path
// as route and hope the content cache can resolve that (it likely can)
return requestedPath;
}
// the Accept-Language header takes precedence over configured domain culture
if (domainAndUri.Culture != null && _requestCultureService.GetRequestedCulture().IsNullOrWhiteSpace())
{
_requestCultureService.SetRequestCulture(domainAndUri.Culture);
}
// when resolving content from a configured domain, the content cache expects the content route
// to be "{domain content ID}/{content path}", which is what we construct here
return GetContentRoute(domainAndUri, contentRoute);
}
}
@@ -1,59 +0,0 @@
using Microsoft.AspNetCore.Http;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.DeliveryApi;
using Umbraco.Cms.Core.Models.PublishedContent;
using Umbraco.Cms.Core.PublishedCache;
using Umbraco.Extensions;
namespace Umbraco.Cms.Api.Delivery.Services;
internal sealed class RequestStartItemProvider : RequestHeaderHandler, IRequestStartItemProvider
{
private readonly IPublishedSnapshotAccessor _publishedSnapshotAccessor;
private readonly IVariationContextAccessor _variationContextAccessor;
// this provider lifetime is Scope, so we can cache this as a field
private IPublishedContent? _requestedStartContent;
public RequestStartItemProvider(
IHttpContextAccessor httpContextAccessor,
IPublishedSnapshotAccessor publishedSnapshotAccessor,
IVariationContextAccessor variationContextAccessor)
: base(httpContextAccessor)
{
_publishedSnapshotAccessor = publishedSnapshotAccessor;
_variationContextAccessor = variationContextAccessor;
}
/// <inheritdoc/>
public IPublishedContent? GetStartItem()
{
if (_requestedStartContent != null)
{
return _requestedStartContent;
}
var headerValue = RequestedStartItem()?.Trim(Constants.CharArrays.ForwardSlash);
if (headerValue.IsNullOrWhiteSpace())
{
return null;
}
if (_publishedSnapshotAccessor.TryGetPublishedSnapshot(out IPublishedSnapshot? publishedSnapshot) == false ||
publishedSnapshot?.Content == null)
{
return null;
}
IEnumerable<IPublishedContent> rootContent = publishedSnapshot.Content.GetAtRoot();
_requestedStartContent = Guid.TryParse(headerValue, out Guid key)
? rootContent.FirstOrDefault(c => c.Key == key)
: rootContent.FirstOrDefault(c => c.UrlSegment(_variationContextAccessor).InvariantEquals(headerValue));
return _requestedStartContent;
}
/// <inheritdoc/>
public string? RequestedStartItem() => GetHeaderValue("Start-Item");
}
@@ -1,63 +0,0 @@
using Microsoft.AspNetCore.Http;
using Umbraco.Cms.Core.DeliveryApi;
using Umbraco.Cms.Core.Models.PublishedContent;
using Umbraco.Cms.Core.PublishedCache;
using Umbraco.Cms.Core.Routing;
using Umbraco.Extensions;
namespace Umbraco.Cms.Api.Delivery.Services;
internal abstract class RoutingServiceBase
{
private readonly IPublishedSnapshotAccessor _publishedSnapshotAccessor;
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly IRequestStartItemProviderAccessor _requestStartItemProviderAccessor;
protected RoutingServiceBase(
IPublishedSnapshotAccessor publishedSnapshotAccessor,
IHttpContextAccessor httpContextAccessor,
IRequestStartItemProviderAccessor requestStartItemProviderAccessor)
{
_publishedSnapshotAccessor = publishedSnapshotAccessor;
_httpContextAccessor = httpContextAccessor;
_requestStartItemProviderAccessor = requestStartItemProviderAccessor;
}
protected Uri GetDefaultRequestUri(string requestedPath)
{
HttpRequest? request = _httpContextAccessor.HttpContext?.Request;
if (request == null)
{
throw new InvalidOperationException("Could not obtain an HTTP request context");
}
// construct the (assumed) absolute URL for the requested content
return new Uri($"{request.Scheme}://{request.Host}{requestedPath}", UriKind.Absolute);
}
protected static string GetContentRoute(DomainAndUri domainAndUri, Uri contentRoute)
=> $"{domainAndUri.ContentId}{DomainUtilities.PathRelativeToDomain(domainAndUri.Uri, contentRoute.AbsolutePath)}";
protected DomainAndUri? GetDomainAndUriForRoute(Uri contentUrl)
{
IDomainCache? domainCache = _publishedSnapshotAccessor.GetRequiredPublishedSnapshot().Domains;
if (domainCache == null)
{
throw new InvalidOperationException("Could not obtain the domain cache in the current context");
}
IEnumerable<Domain> domains = domainCache.GetAll(false);
return DomainUtilities.SelectDomain(domains, contentUrl, defaultCulture: domainCache.DefaultCulture);
}
protected IPublishedContent? GetStartItem()
{
if (_requestStartItemProviderAccessor.TryGetValue(out IRequestStartItemProvider? requestStartItemProvider) is false)
{
throw new InvalidOperationException($"Could not obtain an {nameof(IRequestStartItemProvider)} instance");
}
return requestStartItemProvider.GetStartItem();
}
}
@@ -1,17 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<Title>Umbraco CMS - Delivery API</Title>
<Description>Contains the presentation layer for the Umbraco CMS Delivery API.</Description>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Umbraco.Cms.Api.Common\Umbraco.Cms.Api.Common.csproj" />
<ProjectReference Include="..\Umbraco.Web.Common\Umbraco.Web.Common.csproj" />
</ItemGroup>
<ItemGroup>
<AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleTo">
<_Parameter1>Umbraco.Tests.UnitTests</_Parameter1>
</AssemblyAttribute>
</ItemGroup>
</Project>

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