Compare commits

...
109 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
161 changed files with 4650 additions and 2871 deletions
+1
View File
@@ -49,4 +49,5 @@
<PropertyGroup>
<GitVersionBaseDirectory>$(MSBuildThisFileDirectory)</GitVersionBaseDirectory>
</PropertyGroup>
</Project>
+45 -54
View File
@@ -41,9 +41,13 @@ parameters:
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: 14.18.1
nodeVersion: 20
dotnetVersion: 6.x
dotnetIncludePreviewVersions: false
solution: umbraco.sln
@@ -69,6 +73,7 @@ stages:
steps:
- task: NodeTool@0
displayName: Use Node.js $(nodeVersion)
retryCountOnTaskFailure: 3
inputs:
versionSpec: $(nodeVersion)
- task: Cache@2
@@ -81,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
@@ -100,44 +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 "varsion: $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
- task: PowerShell@2
displayName: Prepare nupkg
inputs:
targetType: inline
script: |
$umbracoVersion = "$(Build.BuildNumber)" -replace "\+",".g"
$templatePaths = Get-ChildItem 'templates/**/.template.config/template.json'
foreach ($templatePath in $templatePaths) {
$a = Get-Content $templatePath -Raw | ConvertFrom-Json
if ($a.symbols -and $a.symbols.UmbracoVersion) {
$a.symbols.UmbracoVersion.defaultValue = $umbracoVersion
$a | ConvertTo-Json -Depth 32 | Set-Content $templatePath
}
}
dotnet pack $(solution) --configuration $(buildConfiguration) -p:BuildProjectReferences=false --output $(Build.ArtifactStagingDirectory)/nupkg
- 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:
@@ -150,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:
@@ -208,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
@@ -279,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:
@@ -312,7 +288,7 @@ stages:
command: test
projects: '**/*.Tests.Integration.csproj'
testRunTitle: Integration Tests SQLite - $(Agent.OS)
${{ if or( parameters.forceReleaseTestFilter, startsWith(variables['Build.SourceBranch'], 'refs/heads/release/')) }}:
${{ if or(variables.releaseTestFilter, parameters.forceReleaseTestFilter) }}:
arguments: '--configuration $(buildConfiguration) --no-build ${{parameters.integrationReleaseTestFilter}}'
${{ else }}:
arguments: '--configuration $(buildConfiguration) ${{parameters.integrationNonReleaseTestFilter}}'
@@ -326,7 +302,7 @@ stages:
command: test
projects: '**/*.Tests.Integration.csproj'
testRunTitle: Integration Tests SQLite - $(Agent.OS)
${{ if or( parameters.forceReleaseTestFilter, startsWith(variables['Build.SourceBranch'], 'refs/heads/release/')) }}:
${{ if or(variables.releaseTestFilter, parameters.forceReleaseTestFilter) }}:
arguments: '--configuration $(buildConfiguration) --no-build ${{parameters.nonWindowsIntegrationReleaseTestFilter}}'
${{ else }}:
arguments: '--configuration $(buildConfiguration) ${{parameters.nonWindowsIntegrationNonReleaseTestFilter}}'
@@ -337,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:
@@ -348,7 +324,7 @@ stages:
Linux:
vmImage: 'ubuntu-latest'
testDb: SqlServer
connectionString: 'Server=localhost,1433;User Id=sa;Password=$(SA_PASSWORD);'
connectionString: 'Server=localhost,1433;User Id=sa;Password=$(SA_PASSWORD);TrustServerCertificate=true'
pool:
vmImage: $(vmImage)
variables:
@@ -359,6 +335,11 @@ stages:
inputs:
artifact: build_output
path: $(Build.SourcesDirectory)
- task: UseDotNet@2
displayName: Use .NET $(dotnetVersion)
inputs:
version: $(dotnetVersion)
includePreviewVersions: $(dotnetIncludePreviewVersions)
- powershell: sqllocaldb start mssqllocaldb
displayName: Start localdb (Windows only)
condition: and(succeeded(), eq(variables['Agent.OS'], 'Windows_NT'))
@@ -372,7 +353,7 @@ stages:
command: test
projects: '**/*.Tests.Integration.csproj'
testRunTitle: Integration Tests SQL Server - $(Agent.OS)
${{ if or( parameters.forceReleaseTestFilter, startsWith(variables['Build.SourceBranch'], 'refs/heads/release/')) }}:
${{ if or(variables.releaseTestFilter, parameters.forceReleaseTestFilter) }}:
arguments: '--configuration $(buildConfiguration) --no-build ${{parameters.integrationReleaseTestFilter}}'
${{ else }}:
arguments: '--configuration $(buildConfiguration) --no-build ${{parameters.integrationNonReleaseTestFilter}}'
@@ -387,7 +368,7 @@ stages:
command: test
projects: '**/*.Tests.Integration.csproj'
testRunTitle: Integration Tests SQL Server - $(Agent.OS)
${{ if or( parameters.forceReleaseTestFilter, startsWith(variables['Build.SourceBranch'], 'refs/heads/release/')) }}:
${{ if or(variables.releaseTestFilter, parameters.forceReleaseTestFilter) }}:
arguments: '--configuration $(buildConfiguration) --no-build ${{parameters.nonWindowsIntegrationReleaseTestFilter}}'
${{ else }}:
arguments: '--configuration $(buildConfiguration) --no-build ${{parameters.nonWindowsIntegrationNonReleaseTestFilter}}'
@@ -418,6 +399,7 @@ stages:
dockerImageName: umbraco-linux
Windows:
vmImage: 'windows-latest'
DOTNET_GENERATE_ASPNET_CERTIFICATE: true # Automatically generate HTTPS development certificate on Windows
# Enable console logging in Release mode
Serilog__WriteTo__0__Name: Async
Serilog__WriteTo__0__Args__configure__0__Name: Console
@@ -442,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
@@ -507,6 +490,8 @@ stages:
workingDirectory: tests/Umbraco.Tests.AcceptanceTest
env:
CI: true
CommitId: $(Build.SourceVersion)
AgentOs: $(Agent.OS)
- pwsh: |
docker logs $(dockerImageName) > $(Build.ArtifactStagingDirectory)/playwright.log 2>&1
docker stop $(dockerImageName)
@@ -545,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
@@ -562,16 +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
@@ -592,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
+4 -3
View File
@@ -1,5 +1,6 @@
name: Nightly_$(TeamProject)_$(Build.DefinitionName)_$(SourceBranchName)_$(Date:yyyyMMdd)$(Rev:.r)
pr: none
trigger: none
schedules:
@@ -7,13 +8,13 @@ schedules:
displayName: Daily midnight build
branches:
include:
- v9/dev
- v10/dev
- v12/dev
- v13/dev
- v14/dev
steps:
- checkout: none
- task: TriggerBuild@4
inputs:
definitionIsInCurrentTeamProject: true
@@ -25,10 +26,10 @@ steps:
useSameBranch: true
waitForQueuedBuildsToFinish: false
storeInEnvironmentVariable: false
templateParameters: 'sqlServerIntegrationTests: true, forceReleaseTestFilter: true'
templateParameters: 'sqlServerIntegrationTests: true, forceReleaseTestFilter: true, myGetDeploy: true, isNightly: true'
authenticationMethod: 'OAuth Token'
enableBuildInQueueCondition: false
dependentOnSuccessfulBuildCondition: false
dependentOnFailedBuildCondition: false
checkbuildsoncurrentbranch: false
failTaskIfConditionsAreNotFulfilled: false
failTaskIfConditionsAreNotFulfilled: false
+6
View File
@@ -0,0 +1,6 @@
{
"sdk": {
"version": "6.0.300",
"rollForward": "latestFeature"
}
}
+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>
+4 -3
View File
@@ -3,16 +3,17 @@
<OutputType>Exe</OutputType>
<IsPackable>false</IsPackable>
<EnablePackageValidation>false</EnablePackageValidation>
<ManagePackageVersionsCentrally>false</ManagePackageVersionsCentrally>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="CommandLineParser" Version="2.9.1" />
<PackageReference Include="NJsonSchema" Version="10.7.2" />
<PackageReference Include="NJsonSchema" Version="10.9.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Umbraco.Core\Umbraco.Core.csproj" />
<PackageReference Include="Umbraco.Deploy.Core" Version="10.2.4" />
<PackageReference Include="Umbraco.Forms.Core" Version="10.5.1" />
<PackageReference Include="Umbraco.Deploy.Core" Version="10.4.0" />
<PackageReference Include="Umbraco.Forms.Core" Version="10.5.4" />
</ItemGroup>
</Project>
@@ -4,6 +4,7 @@
<Description>Contains the presentation layer for the Umbraco CMS Management API.</Description>
<IsPackable>false</IsPackable>
<EnablePackageValidation>false</EnablePackageValidation>
<ManagePackageVersionsCentrally>false</ManagePackageVersionsCentrally>
</PropertyGroup>
<ItemGroup>
@@ -12,7 +13,7 @@
<PackageReference Include="Microsoft.AspNetCore.Mvc.Versioning.ApiExplorer" Version="5.0.0" />
<PackageReference Include="NSwag.AspNetCore" Version="13.16.1" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Umbraco.Infrastructure\Umbraco.Infrastructure.csproj" />
<ProjectReference Include="..\Umbraco.New.Cms.Core\Umbraco.New.Cms.Core.csproj" />
@@ -20,7 +21,7 @@
<ProjectReference Include="..\Umbraco.New.Cms.Web.Common\Umbraco.New.Cms.Web.Common.csproj" />
<ProjectReference Include="..\Umbraco.Web.Common\Umbraco.Web.Common.csproj" />
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="OpenApi.json" />
</ItemGroup>
@@ -134,9 +134,10 @@ public class SqlServerDistributedLockingMechanism : IDistributedLockingMechanism
const string query = "SELECT value FROM umbracoLock WITH (REPEATABLEREAD) WHERE id=@id";
db.Execute("SET LOCK_TIMEOUT " + _timeout.TotalMilliseconds + ";");
var lockTimeoutQuery = $"SET LOCK_TIMEOUT {_timeout.TotalMilliseconds}";
var i = db.ExecuteScalar<int?>(query, new { id = LockId });
// execute the lock timeout query and the actual query in a single server roundtrip
var i = db.ExecuteScalar<int?>($"{lockTimeoutQuery};{query}", new { id = LockId });
if (i == null)
{
@@ -169,9 +170,10 @@ public class SqlServerDistributedLockingMechanism : IDistributedLockingMechanism
const string query =
@"UPDATE umbracoLock WITH (REPEATABLEREAD) SET value = (CASE WHEN (value=1) THEN -1 ELSE 1 END) WHERE id=@id";
db.Execute("SET LOCK_TIMEOUT " + _timeout.TotalMilliseconds + ";");
var lockTimeoutQuery = $"SET LOCK_TIMEOUT {_timeout.TotalMilliseconds}";
var i = db.Execute(query, new { id = LockId });
// execute the lock timeout query and the actual query in a single server roundtrip
var i = db.Execute($"{lockTimeoutQuery};{query}", new { id = LockId });
if (i == 0)
{
@@ -154,7 +154,7 @@ public class SqliteDistributedLockingMechanism : IDistributedLockingMechanism
try
{
var i = command.ExecuteNonQuery();
var i = db.ExecuteNonQuery(command);
if (i == 0)
{
@@ -5,7 +5,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Data.Sqlite" Version="6.0.5" />
<PackageReference Include="Microsoft.Data.Sqlite" />
</ItemGroup>
<ItemGroup>
+1
View File
@@ -4,6 +4,7 @@
<Description>Installs Umbraco CMS with all default dependencies in your ASP.NET Core project.</Description>
<IncludeBuildOutput>false</IncludeBuildOutput>
<IncludeSymbols>false</IncludeSymbols>
<ManagePackageVersionsCentrally>false</ManagePackageVersionsCentrally>
</PropertyGroup>
<ItemGroup>
@@ -84,8 +84,8 @@ public sealed class ContentCacheRefresher : PayloadCacheRefresherBase<ContentCac
isolatedCache.ClearOfType<IContent>((k, v) => v.Path?.Contains(pathid) ?? false);
}
// if the item is being completely removed, we need to refresh the domains cache if any domain was assigned to the content
if (payload.ChangeTypes.HasTypesAny(TreeChangeTypes.Remove))
// if the item is not a blueprint and is being completely removed, we need to refresh the domains cache if any domain was assigned to the content
if (payload.Blueprint is false && payload.ChangeTypes.HasTypesAny(TreeChangeTypes.Remove))
{
idsRemoved.Add(payload.Id);
}
@@ -120,7 +120,11 @@ public sealed class ContentCacheRefresher : PayloadCacheRefresherBase<ContentCac
// should rename it, and then, this is only for Deploy, and then, ???
// if (Suspendable.PageCacheRefresher.CanUpdateDocumentCache)
// ...
NotifyPublishedSnapshotService(_publishedSnapshotService, AppCaches, payloads);
if (payloads.Any(x => x.Blueprint is false))
{
// Only notify if the payload contains actual (non-blueprint) contents
NotifyPublishedSnapshotService(_publishedSnapshotService, AppCaches, payloads);
}
base.Refresh(payloads);
}
@@ -157,8 +161,13 @@ public sealed class ContentCacheRefresher : PayloadCacheRefresherBase<ContentCac
}
}
// TODO (V14): Change into a record
public class JsonPayload
{
public JsonPayload()
{ }
[Obsolete("Use the default constructor and property initializers.")]
public JsonPayload(int id, Guid? key, TreeChangeTypes changeTypes)
{
Id = id;
@@ -166,11 +175,13 @@ public sealed class ContentCacheRefresher : PayloadCacheRefresherBase<ContentCac
ChangeTypes = changeTypes;
}
public int Id { get; }
public int Id { get; init; }
public Guid? Key { get; }
public Guid? Key { get; init; }
public TreeChangeTypes ChangeTypes { get; }
public TreeChangeTypes ChangeTypes { get; init; }
public bool Blueprint { get; init; }
}
#endregion
@@ -134,8 +134,14 @@ public sealed class LanguageCacheRefresher : PayloadCacheRefresherBase<LanguageC
ContentCacheRefresher.RefreshContentTypes(AppCaches); // we need to evict all IContent items
// now refresh all nucache
ContentCacheRefresher.JsonPayload[] clearContentPayload =
new[] { new ContentCacheRefresher.JsonPayload(0, null, TreeChangeTypes.RefreshAll) };
ContentCacheRefresher.JsonPayload[] clearContentPayload = new[]
{
new ContentCacheRefresher.JsonPayload()
{
ChangeTypes = TreeChangeTypes.RefreshAll
}
};
ContentCacheRefresher.NotifyPublishedSnapshotService(_publishedSnapshotService, AppCaches, clearContentPayload);
}
+31 -8
View File
@@ -9,6 +9,9 @@ namespace Umbraco.Cms.Core.Cache;
/// </summary>
public class ObjectCacheAppCache : IAppPolicyCache, IDisposable
{
private static readonly TimeSpan _readLockTimeout = TimeSpan.FromSeconds(5);
private static readonly TimeSpan _writeLockTimeout = TimeSpan.FromSeconds(5);
private readonly ReaderWriterLockSlim _locker = new(LockRecursionPolicy.SupportsRecursion);
private bool _disposedValue;
@@ -33,7 +36,10 @@ public class ObjectCacheAppCache : IAppPolicyCache, IDisposable
Lazy<object?>? result;
try
{
_locker.EnterReadLock();
if (_locker.TryEnterReadLock(_readLockTimeout) is false)
{
throw new TimeoutException("Timeout exceeded to the memory cache when getting item");
}
result = MemoryCache.Get(key) as Lazy<object?>; // null if key not found
}
finally
@@ -195,7 +201,10 @@ public class ObjectCacheAppCache : IAppPolicyCache, IDisposable
{
try
{
_locker.EnterWriteLock();
if (_locker.TryEnterWriteLock(_writeLockTimeout) is false)
{
throw new TimeoutException("Timeout exceeded to the memory cache when clearing item");
}
if (MemoryCache[key] == null)
{
return;
@@ -223,8 +232,10 @@ public class ObjectCacheAppCache : IAppPolicyCache, IDisposable
var isInterface = type.IsInterface;
try
{
_locker.EnterWriteLock();
if (_locker.TryEnterWriteLock(_writeLockTimeout) is false)
{
throw new TimeoutException("Timeout exceeded to the memory cache when clearing by type");
}
// ToArray required to remove
foreach (var key in MemoryCache
.Where(x =>
@@ -259,7 +270,10 @@ public class ObjectCacheAppCache : IAppPolicyCache, IDisposable
{
try
{
_locker.EnterWriteLock();
if (_locker.TryEnterWriteLock(_writeLockTimeout) is false)
{
throw new TimeoutException("Timeout exceeded to the memory cache when clearing by generic type");
}
Type typeOfT = typeof(T);
var isInterface = typeOfT.IsInterface;
@@ -296,7 +310,10 @@ public class ObjectCacheAppCache : IAppPolicyCache, IDisposable
{
try
{
_locker.EnterWriteLock();
if (_locker.TryEnterWriteLock(_writeLockTimeout) is false)
{
throw new TimeoutException("Timeout exceeded to the memory cache when clearing generic type with predicate");
}
Type typeOfT = typeof(T);
var isInterface = typeOfT.IsInterface;
@@ -338,7 +355,10 @@ public class ObjectCacheAppCache : IAppPolicyCache, IDisposable
{
try
{
_locker.EnterWriteLock();
if (_locker.TryEnterWriteLock(_writeLockTimeout) is false)
{
throw new TimeoutException("Timeout exceeded to the memory cache when clearing with prefix");
}
// ToArray required to remove
foreach (var key in MemoryCache
@@ -365,7 +385,10 @@ public class ObjectCacheAppCache : IAppPolicyCache, IDisposable
try
{
_locker.EnterWriteLock();
if (_locker.TryEnterWriteLock(_writeLockTimeout) is false)
{
throw new TimeoutException("Timeout exceeded to the memory cach when clearing by regex");
}
// ToArray required to remove
foreach (var key in MemoryCache
@@ -3,30 +3,26 @@ using System.Collections;
namespace Umbraco.Cms.Core.Composing;
/// <summary>
/// Provides a base class for builder collections.
/// Provides a base class for builder collections.
/// </summary>
/// <typeparam name="TItem">The type of the items.</typeparam>
public abstract class BuilderCollectionBase<TItem> : IBuilderCollection<TItem>
{
private readonly LazyReadOnlyCollection<TItem> _items;
/// Initializes a new instance of the
/// <see cref="BuilderCollectionBase{TItem}" />
/// with items.
/// <summary>
/// Initializes a new instance of the <see cref="BuilderCollectionBase{TItem}" /> with items.
/// </summary>
/// <param name="items">The items.</param>
public BuilderCollectionBase(Func<IEnumerable<TItem>> items) => _items = new LazyReadOnlyCollection<TItem>(items);
public BuilderCollectionBase(Func<IEnumerable<TItem>> items)
=> _items = new LazyReadOnlyCollection<TItem>(items);
/// <inheritdoc />
public int Count => _items.Count;
/// <summary>
/// Gets an enumerator.
/// </summary>
/// <inheritdoc />
public IEnumerator<TItem> GetEnumerator() => _items.GetEnumerator();
/// <summary>
/// Gets an enumerator.
/// </summary>
/// <inheritdoc />
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}
@@ -1,13 +1,16 @@
namespace Umbraco.Cms.Core.Composing;
/// <summary>
/// Represents a builder collection, ie an immutable enumeration of items.
/// Represents a builder collection, ie an immutable enumeration of items.
/// </summary>
/// <typeparam name="TItem">The type of the items.</typeparam>
public interface IBuilderCollection<out TItem> : IEnumerable<TItem>
{
/// <summary>
/// Gets the number of items in the collection.
/// Gets the number of items in the collection.
/// </summary>
/// <value>
/// The count.
/// </value>
int Count { get; }
}
@@ -2,6 +2,7 @@
// See LICENSE for more details.
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
namespace Umbraco.Cms.Core.Configuration.Models;
@@ -24,6 +25,8 @@ public class SecuritySettings
internal const int StaticMemberDefaultLockoutTimeInMinutes = 30 * 24 * 60;
internal const int StaticUserDefaultLockoutTimeInMinutes = 30 * 24 * 60;
private const long StaticUserDefaultFailedLoginDurationInMilliseconds = 1000;
private const long StaticUserMinimumFailedLoginDurationInMilliseconds = 250;
/// <summary>
/// Gets or sets a value indicating whether to keep the user logged in.
@@ -109,4 +112,26 @@ public class SecuritySettings
[Obsolete("Use ContentSettings.AllowEditFromInvariant instead")]
[DefaultValue(StaticAllowEditInvariantFromNonDefault)]
public bool AllowEditInvariantFromNonDefault { get; set; } = StaticAllowEditInvariantFromNonDefault;
/// <summary>
/// Gets or sets the default duration (in milliseconds) of failed login attempts.
/// </summary>
/// <value>
/// The default duration (in milliseconds) of failed login attempts.
/// </value>
/// <remarks>
/// The user login endpoint ensures that failed login attempts take at least as long as the average successful login.
/// However, if no successful logins have occurred, this value is used as the default duration.
/// </remarks>
[DefaultValue(StaticUserDefaultFailedLoginDurationInMilliseconds)]
public long UserDefaultFailedLoginDurationInMilliseconds { get; set; } = StaticUserDefaultFailedLoginDurationInMilliseconds;
/// <summary>
/// Gets or sets the minimum duration (in milliseconds) of failed login attempts.
/// </summary>
/// <value>
/// The minimum duration (in milliseconds) of failed login attempts.
/// </value>
[DefaultValue(StaticUserMinimumFailedLoginDurationInMilliseconds)]
public long UserMinimumFailedLoginDurationInMilliseconds { get; set; } = StaticUserMinimumFailedLoginDurationInMilliseconds;
}
@@ -318,8 +318,9 @@ namespace Umbraco.Cms.Core.DependencyInjection
Services.AddSingleton<ConflictingPackageData>();
Services.AddSingleton<CompiledPackageXmlParser>();
// Register a noop IHtmlSanitizer to be replaced
// Register a noop IHtmlSanitizer & IMarkdownSanitizer to be replaced
Services.AddUnique<IHtmlSanitizer, NoopHtmlSanitizer>();
Services.AddUnique<IMarkdownSanitizer, NoopMarkdownSanitizer>();
Services.AddUnique<IPropertyTypeUsageService, PropertyTypeUsageService>();
Services.AddUnique<IDataTypeUsageService, DataTypeUsageService>();
@@ -134,27 +134,6 @@ public static class PublishedElementExtensions
#endregion
#region CheckVariation
/// <summary>
/// Method to check if VariationContext culture differs from culture parameter, if so it will update the VariationContext for the PublishedValueFallback.
/// </summary>
/// <param name="publishedValueFallback">The requested PublishedValueFallback.</param>
/// <param name="culture">The requested culture.</param>
/// <param name="segment">The requested segment.</param>
/// <returns></returns>
private static void EventuallyUpdateVariationContext(IPublishedValueFallback publishedValueFallback, string? culture, string? segment)
{
IVariationContextAccessor? variationContextAccessor = publishedValueFallback.VariationContextAccessor;
//If there is a difference in requested culture and the culture that is set in the VariationContext, it will pick wrong localized content.
//This happens for example using links to localized content in a RichText Editor.
if (!string.IsNullOrEmpty(culture) && variationContextAccessor?.VariationContext?.Culture != culture)
{
variationContextAccessor!.VariationContext = new VariationContext(culture, segment);
}
}
#endregion
#region Value<T>
/// <summary>
@@ -195,8 +174,6 @@ public static class PublishedElementExtensions
{
IPublishedProperty? property = content.GetProperty(alias);
EventuallyUpdateVariationContext(publishedValueFallback, culture, segment);
// if we have a property, and it has a value, return that value
if (property != null && property.HasValue(culture, segment))
{
+1 -1
View File
@@ -358,7 +358,7 @@ namespace Umbraco.Cms.Core.IO
// nothing prevents us to reach the file, security-wise, yet it is outside
// this filesystem's root - throw
throw new UnauthorizedAccessException($"File original: [{originalPath}] full: [{path}] is outside this filesystem's root.");
throw new UnauthorizedAccessException($"Requested path {originalPath} is outside this filesystem's root.");
}
/// <summary>
+19 -19
View File
@@ -7,19 +7,21 @@ namespace Umbraco.Cms.Core.IO;
internal class ShadowWrapper : IFileSystem, IFileProviderFactory
{
private static readonly string ShadowFsPath = Constants.SystemDirectories.TempData.EnsureEndsWith('/') + "ShadowFs";
private readonly IHostingEnvironment _hostingEnvironment;
private readonly IIOHelper _ioHelper;
private const string ShadowFsPath = "ShadowFs";
private readonly Func<bool?>? _isScoped;
private readonly IIOHelper _ioHelper;
private readonly IHostingEnvironment _hostingEnvironment;
private readonly ILoggerFactory _loggerFactory;
private readonly string _shadowPath;
private readonly Func<bool?>? _isScoped;
private string? _shadowDir;
private ShadowFileSystem? _shadowFileSystem;
public ShadowWrapper(IFileSystem innerFileSystem, IIOHelper ioHelper, IHostingEnvironment hostingEnvironment, ILoggerFactory loggerFactory, string shadowPath, Func<bool?>? isScoped = null)
{
InnerFileSystem = innerFileSystem;
_ioHelper = ioHelper ?? throw new ArgumentNullException(nameof(ioHelper));
_hostingEnvironment = hostingEnvironment ?? throw new ArgumentNullException(nameof(hostingEnvironment));
_loggerFactory = loggerFactory;
@@ -35,18 +37,19 @@ internal class ShadowWrapper : IFileSystem, IFileProviderFactory
{
get
{
if (_isScoped is not null && _shadowFileSystem is not null)
Func<bool?>? isScoped = _isScoped;
if (isScoped is not null && _shadowFileSystem is not null)
{
var isScoped = _isScoped!();
bool? scoped = isScoped();
// if the filesystem is created *after* shadowing starts, it won't be shadowing
// better not ignore that situation and raised a meaningful (?) exception
if (isScoped.HasValue && isScoped.Value && _shadowFileSystem == null)
// better not ignore that situation and raise a meaningful (?) exception
if (scoped.HasValue && scoped.Value && _shadowFileSystem == null)
{
throw new Exception("The filesystems are shadowing, but this filesystem is not.");
}
return isScoped.HasValue && isScoped.Value
return scoped.HasValue && scoped.Value
? _shadowFileSystem
: InnerFileSystem;
}
@@ -56,8 +59,7 @@ internal class ShadowWrapper : IFileSystem, IFileProviderFactory
}
/// <inheritdoc />
public IFileProvider? Create() =>
InnerFileSystem.TryCreateFileProvider(out IFileProvider? fileProvider) ? fileProvider : null;
public IFileProvider? Create() => InnerFileSystem.TryCreateFileProvider(out IFileProvider? fileProvider) ? fileProvider : null;
public IEnumerable<string> GetDirectories(string path) => FileSystem.GetDirectories(path);
@@ -69,8 +71,7 @@ internal class ShadowWrapper : IFileSystem, IFileProviderFactory
public void AddFile(string path, Stream stream) => FileSystem.AddFile(path, stream);
public void AddFile(string path, Stream stream, bool overrideExisting) =>
FileSystem.AddFile(path, stream, overrideExisting);
public void AddFile(string path, Stream stream, bool overrideExisting) => FileSystem.AddFile(path, stream, overrideExisting);
public IEnumerable<string> GetFiles(string path) => FileSystem.GetFiles(path);
@@ -107,8 +108,7 @@ internal class ShadowWrapper : IFileSystem, IFileProviderFactory
{
var id = GuidUtils.ToBase32String(Guid.NewGuid(), idLength);
var virt = ShadowFsPath + "/" + id;
var shadowDir = hostingEnvironment.MapPathContentRoot(virt);
var shadowDir = Path.Combine(hostingEnvironment.LocalTempPath, ShadowFsPath, id);
if (Directory.Exists(shadowDir))
{
continue;
@@ -129,10 +129,10 @@ internal class ShadowWrapper : IFileSystem, IFileProviderFactory
// note: no thread-safety here, because ShadowFs is thread-safe due to the check
// on ShadowFileSystemsScope.None - and if None is false then we should be running
// in a single thread anyways
var virt = Path.Combine(ShadowFsPath, id, _shadowPath);
_shadowDir = _hostingEnvironment.MapPathContentRoot(virt);
var rootUrl = Path.Combine(ShadowFsPath, id, _shadowPath);
_shadowDir = Path.Combine(_hostingEnvironment.LocalTempPath, rootUrl);
Directory.CreateDirectory(_shadowDir);
var tempfs = new PhysicalFileSystem(_ioHelper, _hostingEnvironment, _loggerFactory.CreateLogger<PhysicalFileSystem>(), _shadowDir, _hostingEnvironment.ToAbsolute(virt));
var tempfs = new PhysicalFileSystem(_ioHelper, _hostingEnvironment, _loggerFactory.CreateLogger<PhysicalFileSystem>(), _shadowDir, rootUrl);
_shadowFileSystem = new ShadowFileSystem(InnerFileSystem, tempfs);
}
@@ -160,7 +160,7 @@ internal class ShadowWrapper : IFileSystem, IFileProviderFactory
// shadowPath make be path/to/dir, remove each
dir = dir!.Replace('/', Path.DirectorySeparatorChar);
var min = _hostingEnvironment.MapPathContentRoot(ShadowFsPath).Length;
var min = Path.Combine(_hostingEnvironment.LocalTempPath, ShadowFsPath).Length;
var pos = dir.LastIndexOf(Path.DirectorySeparatorChar);
while (pos > min)
{
@@ -1,49 +1,88 @@
namespace Umbraco.Cms.Core.Models.Editors;
/// <summary>
/// Used to track reference to other entities in a property value
/// Used to track a reference to another entity in a property value.
/// </summary>
public struct UmbracoEntityReference : IEquatable<UmbracoEntityReference>
{
private static readonly UmbracoEntityReference _empty = new(UnknownTypeUdi.Instance, string.Empty);
/// <summary>
/// Initializes a new instance of the <see cref="UmbracoEntityReference" /> struct.
/// </summary>
/// <param name="udi">The UDI.</param>
/// <param name="relationTypeAlias">The relation type alias.</param>
public UmbracoEntityReference(Udi udi, string relationTypeAlias)
{
Udi = udi ?? throw new ArgumentNullException(nameof(udi));
RelationTypeAlias = relationTypeAlias ?? throw new ArgumentNullException(nameof(relationTypeAlias));
}
/// <summary>
/// Initializes a new instance of the <see cref="UmbracoEntityReference" /> struct for a document or media item.
/// </summary>
/// <param name="udi">The UDI.</param>
public UmbracoEntityReference(Udi udi)
{
Udi = udi ?? throw new ArgumentNullException(nameof(udi));
switch (udi.EntityType)
{
case Constants.UdiEntityType.Document:
RelationTypeAlias = Constants.Conventions.RelationTypes.RelatedDocumentAlias;
break;
case Constants.UdiEntityType.Media:
RelationTypeAlias = Constants.Conventions.RelationTypes.RelatedMediaAlias;
break;
default:
RelationTypeAlias = Constants.Conventions.RelationTypes.RelatedDocumentAlias;
// No relation type alias convention for this entity type, so leave it empty
RelationTypeAlias = string.Empty;
break;
}
}
/// <summary>
/// Gets the UDI.
/// </summary>
/// <value>
/// The UDI.
/// </value>
public Udi Udi { get; }
public static UmbracoEntityReference Empty() => _empty;
public static bool IsEmpty(UmbracoEntityReference reference) => reference == Empty();
/// <summary>
/// Gets the relation type alias.
/// </summary>
/// <value>
/// The relation type alias.
/// </value>
public string RelationTypeAlias { get; }
public static bool operator ==(UmbracoEntityReference left, UmbracoEntityReference right) => left.Equals(right);
/// <summary>
/// Gets an empty reference.
/// </summary>
/// <returns>
/// An empty reference.
/// </returns>
public static UmbracoEntityReference Empty() => _empty;
/// <summary>
/// Determines whether the specified reference is empty.
/// </summary>
/// <param name="reference">The reference.</param>
/// <returns>
/// <c>true</c> if the specified reference is empty; otherwise, <c>false</c>.
/// </returns>
public static bool IsEmpty(UmbracoEntityReference reference) => reference == Empty();
/// <inheritdoc />
public override bool Equals(object? obj) => obj is UmbracoEntityReference reference && Equals(reference);
/// <inheritdoc />
public bool Equals(UmbracoEntityReference other) =>
EqualityComparer<Udi>.Default.Equals(Udi, other.Udi) &&
RelationTypeAlias == other.RelationTypeAlias;
/// <inheritdoc />
public override int GetHashCode()
{
var hashCode = -487348478;
@@ -52,5 +91,9 @@ public struct UmbracoEntityReference : IEquatable<UmbracoEntityReference>
return hashCode;
}
/// <inheritdoc />
public static bool operator ==(UmbracoEntityReference left, UmbracoEntityReference right) => left.Equals(right);
/// <inheritdoc />
public static bool operator !=(UmbracoEntityReference left, UmbracoEntityReference right) => !(left == right);
}
@@ -5,6 +5,7 @@ namespace Umbraco.Cms.Core.Models.PublishedContent;
/// </summary>
public interface IPublishedValueFallback
{
[Obsolete("Scheduled for removal in v14")]
/// <summary>
/// VariationContextAccessor that is not required to be implemented, therefore throws NotImplementedException as default.
/// </summary>
@@ -20,6 +20,7 @@ public class PublishedValueFallback : IPublishedValueFallback
_variationContextAccessor = variationContextAccessor;
}
[Obsolete("Scheduled for removal in v14")]
public IVariationContextAccessor VariationContextAccessor { get { return _variationContextAccessor; } }
/// <inheritdoc />
@@ -0,0 +1,18 @@
// Copyright (c) Umbraco.
// See LICENSE for more details.
using Umbraco.Cms.Core.Events;
using Umbraco.Cms.Core.Models;
namespace Umbraco.Cms.Core.Notifications;
/// <summary>
/// Notification that is send out when a Content item has been scaffolded from an original item and basic cleaning has been performed
/// </summary>
public sealed class ContentScaffoldedNotification : ScaffoldedNotification<IContent>
{
public ContentScaffoldedNotification(IContent original, IContent scaffold, int parentId, EventMessages messages)
: base(original, scaffold, parentId, messages)
{
}
}
@@ -0,0 +1,23 @@
// Copyright (c) Umbraco.
// See LICENSE for more details.
using Umbraco.Cms.Core.Events;
namespace Umbraco.Cms.Core.Notifications;
public abstract class ScaffoldedNotification<T> : CancelableObjectNotification<T>
where T : class
{
protected ScaffoldedNotification(T original, T scaffold, int parentId, EventMessages messages)
: base(original, messages)
{
Scaffold = scaffold;
ParentId = parentId;
}
public T Original => Target;
public T Scaffold { get; }
public int ParentId { get; }
}
@@ -4,64 +4,156 @@ using Umbraco.Cms.Core.Models.Editors;
namespace Umbraco.Cms.Core.PropertyEditors;
/// <summary>
/// Provides a builder collection for <see cref="IDataValueReferenceFactory" /> items.
/// </summary>
public class DataValueReferenceFactoryCollection : BuilderCollectionBase<IDataValueReferenceFactory>
{
public DataValueReferenceFactoryCollection(Func<IEnumerable<IDataValueReferenceFactory>> items)
: base(items)
{
}
// TODO: We could further reduce circular dependencies with PropertyEditorCollection by not having IDataValueReference implemented
// by property editors and instead just use the already built in IDataValueReferenceFactory and/or refactor that into a more normal collection
public IEnumerable<UmbracoEntityReference> GetAllReferences(
IPropertyCollection properties,
PropertyEditorCollection propertyEditors)
{
var trackedRelations = new HashSet<UmbracoEntityReference>();
foreach (IProperty p in properties)
/// <summary>
/// Initializes a new instance of the <see cref="DataValueReferenceFactoryCollection" /> class.
/// </summary>
/// <param name="items">The items.</param>
public DataValueReferenceFactoryCollection(Func<IEnumerable<IDataValueReferenceFactory>> items)
: base(items)
{ }
/// <summary>
/// Gets all unique references from the specified properties.
/// </summary>
/// <param name="properties">The properties.</param>
/// <param name="propertyEditors">The property editors.</param>
/// <returns>
/// The unique references from the specified properties.
/// </returns>
public ISet<UmbracoEntityReference> GetAllReferences(IPropertyCollection properties, PropertyEditorCollection propertyEditors)
{
var references = new HashSet<UmbracoEntityReference>();
// Group by property editor alias to avoid duplicate lookups and optimize value parsing
foreach (var propertyValuesByPropertyEditorAlias in properties.GroupBy(x => x.PropertyType.PropertyEditorAlias, x => x.Values))
{
if (!propertyEditors.TryGet(p.PropertyType.PropertyEditorAlias, out IDataEditor? editor))
if (!propertyEditors.TryGet(propertyValuesByPropertyEditorAlias.Key, out IDataEditor? dataEditor))
{
continue;
}
// TODO: We will need to change this once we support tracking via variants/segments
// for now, we are tracking values from ALL variants
foreach (IPropertyValue propertyVal in p.Values)
// Use distinct values to avoid duplicate parsing of the same value
var values = new HashSet<object?>(properties.Count);
foreach (IPropertyValue propertyValue in propertyValuesByPropertyEditorAlias.SelectMany(x => x))
{
var val = propertyVal.EditedValue;
values.Add(propertyValue.EditedValue);
values.Add(propertyValue.PublishedValue);
}
IDataValueEditor? valueEditor = editor?.GetValueEditor();
if (valueEditor is IDataValueReference reference)
{
IEnumerable<UmbracoEntityReference> refs = reference.GetReferences(val);
foreach (UmbracoEntityReference r in refs)
{
trackedRelations.Add(r);
}
}
references.UnionWith(GetReferences(dataEditor, values));
}
// Loop over collection that may be add to existing property editors
// implementation of GetReferences in IDataValueReference.
// Allows developers to add support for references by a
// package /property editor that did not implement IDataValueReference themselves
foreach (IDataValueReferenceFactory item in this)
{
// Check if this value reference is for this datatype/editor
// Then call it's GetReferences method - to see if the value stored
// in the dataeditor/property has referecnes to media/content items
if (item.IsForEditor(editor))
{
foreach (UmbracoEntityReference r in item.GetDataValueReference().GetReferences(val))
{
trackedRelations.Add(r);
}
}
}
return references;
}
/// <summary>
/// Gets the references.
/// </summary>
/// <param name="dataEditor">The data editor.</param>
/// <param name="values">The values.</param>
/// <returns>
/// The references.
/// </returns>
public IEnumerable<UmbracoEntityReference> GetReferences(IDataEditor dataEditor, params object?[] values)
=> GetReferences(dataEditor, (IEnumerable<object?>)values);
/// <summary>
/// Gets the references.
/// </summary>
/// <param name="dataEditor">The data editor.</param>
/// <param name="values">The values.</param>
/// <returns>
/// The references.
/// </returns>
public IEnumerable<UmbracoEntityReference> GetReferences(IDataEditor dataEditor, IEnumerable<object?> values)
{
// TODO: We will need to change this once we support tracking via variants/segments
// for now, we are tracking values from ALL variants
if (dataEditor.GetValueEditor() is IDataValueReference dataValueReference)
{
foreach (UmbracoEntityReference reference in values.SelectMany(dataValueReference.GetReferences))
{
yield return reference;
}
}
return trackedRelations;
// Loop over collection that may be add to existing property editors
// implementation of GetReferences in IDataValueReference.
// Allows developers to add support for references by a
// package /property editor that did not implement IDataValueReference themselves
foreach (IDataValueReferenceFactory dataValueReferenceFactory in this)
{
// Check if this value reference is for this datatype/editor
// Then call it's GetReferences method - to see if the value stored
// in the dataeditor/property has references to media/content items
if (dataValueReferenceFactory.IsForEditor(dataEditor))
{
IDataValueReference factoryDataValueReference = dataValueReferenceFactory.GetDataValueReference();
foreach (UmbracoEntityReference reference in values.SelectMany(factoryDataValueReference.GetReferences))
{
yield return reference;
}
}
}
}
/// <summary>
/// Gets all relation type aliases that are automatically tracked.
/// </summary>
/// <param name="propertyEditors">The property editors.</param>
/// <returns>
/// All relation type aliases that are automatically tracked.
/// </returns>
public ISet<string> GetAllAutomaticRelationTypesAliases(PropertyEditorCollection propertyEditors)
{
// Always add default automatic relation types
var automaticRelationTypeAliases = new HashSet<string>(Constants.Conventions.RelationTypes.AutomaticRelationTypes);
// Add relation types for all property editors
foreach (IDataEditor dataEditor in propertyEditors)
{
automaticRelationTypeAliases.UnionWith(GetAutomaticRelationTypesAliases(dataEditor));
}
return automaticRelationTypeAliases;
}
/// <summary>
/// Gets the automatic relation types aliases.
/// </summary>
/// <param name="dataEditor">The data editor.</param>
/// <returns>
/// The automatic relation types aliases.
/// </returns>
public IEnumerable<string> GetAutomaticRelationTypesAliases(IDataEditor dataEditor)
{
if (dataEditor.GetValueEditor() is IDataValueReference dataValueReference)
{
// Return custom relation types from value editor implementation
foreach (var alias in dataValueReference.GetAutomaticRelationTypesAliases())
{
yield return alias;
}
}
foreach (IDataValueReferenceFactory dataValueReferenceFactory in this)
{
if (dataValueReferenceFactory.IsForEditor(dataEditor))
{
// Return custom relation types from factory
foreach (var alias in dataValueReferenceFactory.GetDataValueReference().GetAutomaticRelationTypesAliases())
{
yield return alias;
}
}
}
}
}
@@ -9,15 +9,22 @@ namespace Umbraco.Cms.Core.PropertyEditors;
/// </summary>
public class DefaultPropertyIndexValueFactory : IPropertyIndexValueFactory
{
/// <inheritdoc />
public IEnumerable<KeyValuePair<string, IEnumerable<object?>>> GetIndexValues(IProperty property, string? culture, string? segment, bool published, IEnumerable<string> availableCultures)
public IEnumerable<KeyValuePair<string, IEnumerable<object?>>> GetIndexValues(IProperty property, string? culture, string? segment, bool published,
IEnumerable<string> availableCultures, IDictionary<Guid, IContentType> contentTypeDictionary)
{
yield return new KeyValuePair<string, IEnumerable<object?>>(
property.Alias,
property.GetValue(culture, segment, published).Yield());
}
[Obsolete("Use the overload with the availableCultures parameter instead, scheduled for removal in v14")]
/// <inheritdoc />
[Obsolete("Use the non-obsolete overload, scheduled for removal in v14")]
public IEnumerable<KeyValuePair<string, IEnumerable<object?>>> GetIndexValues(IProperty property, string? culture,
string? segment, bool published, IEnumerable<string> availableCultures)
=> GetIndexValues(property, culture, segment, published, availableCultures,
new Dictionary<Guid, IContentType>());
[Obsolete("Use the non-obsolete overload, scheduled for removal in v14")]
public IEnumerable<KeyValuePair<string, IEnumerable<object?>>> GetIndexValues(IProperty property, string? culture, string? segment, bool published)
=> GetIndexValues(property, culture, segment, published, Enumerable.Empty<string>());
=> GetIndexValues(property, culture, segment, published, Enumerable.Empty<string>(), new Dictionary<Guid, IContentType>());
}
@@ -22,9 +22,14 @@ public interface IPropertyIndexValueFactory
/// more than one value for a given field.
/// </para>
/// </remarks>
IEnumerable<KeyValuePair<string, IEnumerable<object?>>> GetIndexValues(IProperty property, string? culture,
string? segment, bool published, IEnumerable<string> availableCultures,
IDictionary<Guid, IContentType> contentTypeDictionary) => GetIndexValues(property, culture, segment, published);
[Obsolete("Use non-obsolete overload, scheduled for removal in v14")]
IEnumerable<KeyValuePair<string, IEnumerable<object?>>> GetIndexValues(IProperty property, string? culture, string? segment, bool published, IEnumerable<string> availableCultures)
=> GetIndexValues(property, culture, segment, published);
[Obsolete("Use the overload with the availableCultures parameter instead, scheduled for removal in v14")]
[Obsolete("Use non-obsolete overload, scheduled for removal in v14")]
IEnumerable<KeyValuePair<string, IEnumerable<object?>>> GetIndexValues(IProperty property, string? culture, string? segment, bool published);
}
@@ -3,6 +3,7 @@ using Microsoft.Extensions.Options;
using Umbraco.Cms.Core.Configuration.Models;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Serialization;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Web.Common.DependencyInjection;
using Umbraco.Extensions;
@@ -39,13 +40,13 @@ public abstract class JsonPropertyIndexValueFactoryBase<TSerialized> : IProperty
}
/// <inheritdoc />
public IEnumerable<KeyValuePair<string, IEnumerable<object?>>> GetIndexValues(
IProperty property,
string? culture,
string? segment,
bool published,
IEnumerable<string> availableCultures)
IEnumerable<string> availableCultures,
IDictionary<Guid, IContentType> contentTypeDictionary)
{
var result = new List<KeyValuePair<string, IEnumerable<object?>>>();
@@ -63,7 +64,7 @@ public abstract class JsonPropertyIndexValueFactoryBase<TSerialized> : IProperty
return result;
}
result.AddRange(Handle(deserializedPropertyValue, property, culture, segment, published, availableCultures));
result.AddRange(Handle(deserializedPropertyValue, property, culture, segment, published, availableCultures, contentTypeDictionary));
}
catch (InvalidCastException)
{
@@ -87,9 +88,31 @@ public abstract class JsonPropertyIndexValueFactoryBase<TSerialized> : IProperty
return summary;
}
/// <inheritdoc />
[Obsolete("Use non-obsolete constructor. This will be removed in Umbraco 14.")]
public IEnumerable<KeyValuePair<string, IEnumerable<object?>>> GetIndexValues(
IProperty property,
string? culture,
string? segment,
bool published,
IEnumerable<string> availableCultures)
=> GetIndexValues(
property,
culture,
segment,
published,
Enumerable.Empty<string>(),
StaticServiceProvider.Instance.GetRequiredService<IContentTypeService>().GetAll().ToDictionary(x=>x.Key));
[Obsolete("Use method overload that has availableCultures, scheduled for removal in v14")]
public IEnumerable<KeyValuePair<string, IEnumerable<object?>>> GetIndexValues(IProperty property, string? culture, string? segment, bool published)
=> GetIndexValues(property, culture, segment, published, Enumerable.Empty<string>());
=> GetIndexValues(
property,
culture,
segment,
published,
Enumerable.Empty<string>(),
StaticServiceProvider.Instance.GetRequiredService<IContentTypeService>().GetAll().ToDictionary(x=>x.Key));
/// <summary>
/// Method to return a list of summary of the content. By default this returns an empty list
@@ -104,7 +127,7 @@ public abstract class JsonPropertyIndexValueFactoryBase<TSerialized> : IProperty
/// <summary>
/// Method that handle the deserialized object.
/// </summary>
[Obsolete("Use the overload with the availableCultures parameter instead, scheduled for removal in v14")]
[Obsolete("Use the non-obsolete overload instead, scheduled for removal in v14")]
protected abstract IEnumerable<KeyValuePair<string, IEnumerable<object?>>> Handle(
TSerialized deserializedPropertyValue,
IProperty property,
@@ -112,6 +135,15 @@ public abstract class JsonPropertyIndexValueFactoryBase<TSerialized> : IProperty
string? segment,
bool published);
[Obsolete("Use the non-obsolete overload instead, scheduled for removal in v14")]
protected virtual IEnumerable<KeyValuePair<string, IEnumerable<object?>>> Handle(
TSerialized deserializedPropertyValue,
IProperty property,
string? culture,
string? segment,
bool published,
IEnumerable<string> availableCultures) => Handle(deserializedPropertyValue, property, culture, segment, published);
/// <summary>
/// Method that handle the deserialized object.
/// </summary>
@@ -121,6 +153,7 @@ public abstract class JsonPropertyIndexValueFactoryBase<TSerialized> : IProperty
string? culture,
string? segment,
bool published,
IEnumerable<string> availableCultures) =>
Handle(deserializedPropertyValue, property, culture, segment, published);
IEnumerable<string> availableCultures,
IDictionary<Guid, IContentType> contentTypeDictionary)
=> Handle(deserializedPropertyValue, property, culture, segment, published, availableCultures);
}
@@ -0,0 +1,39 @@
using Umbraco.Cms.Core.IO;
using Umbraco.Cms.Core.Models.Editors;
using Umbraco.Cms.Core.Security;
using Umbraco.Cms.Core.Serialization;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Strings;
using Umbraco.Extensions;
namespace Umbraco.Cms.Core.PropertyEditors;
/// <summary>
/// A custom value editor to ensure that macro syntax is parsed when being persisted and formatted correctly for
/// display in the editor
/// </summary>
internal class MarkDownPropertyValueEditor : DataValueEditor
{
private readonly IMarkdownSanitizer _markdownSanitizer;
public MarkDownPropertyValueEditor(
ILocalizedTextService localizedTextService,
IShortStringHelper shortStringHelper,
IJsonSerializer jsonSerializer,
IIOHelper ioHelper,
DataEditorAttribute attribute,
IMarkdownSanitizer markdownSanitizer)
: base(localizedTextService, shortStringHelper, jsonSerializer, ioHelper, attribute) => _markdownSanitizer = markdownSanitizer;
public override object? FromEditor(ContentPropertyData editorValue, object? currentValue)
{
if (string.IsNullOrWhiteSpace(editorValue.Value?.ToString()))
{
return null;
}
var sanitized = _markdownSanitizer.Sanitize(editorValue.Value.ToString()!);
return sanitized.NullOrWhiteSpaceAsNull();
}
}
@@ -3,6 +3,7 @@
using Microsoft.Extensions.DependencyInjection;
using Umbraco.Cms.Core.IO;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Web.Common.DependencyInjection;
@@ -50,4 +51,11 @@ public class MarkdownPropertyEditor : DataEditor
/// <inheritdoc />
protected override IConfigurationEditor CreateConfigurationEditor() =>
new MarkdownConfigurationEditor(_ioHelper, _editorConfigurationParser);
/// <summary>
/// Create a custom value editor
/// </summary>
/// <returns></returns>
protected override IDataValueEditor CreateValueEditor() =>
DataValueEditorFactory.Create<MarkDownPropertyValueEditor>(Attribute!);
}
@@ -8,6 +8,12 @@ namespace Umbraco.Cms.Core.PropertyEditors;
public class NoopPropertyIndexValueFactory : IPropertyIndexValueFactory
{
/// <inheritdoc />
public IEnumerable<KeyValuePair<string, IEnumerable<object?>>> GetIndexValues(IProperty property, string? culture, string? segment, bool published,
IEnumerable<string> availableCultures, IDictionary<Guid, IContentType> contentTypeDictionary)
=> Array.Empty<KeyValuePair<string, IEnumerable<object?>>>();
[Obsolete("Use the overload with the availableCultures parameter instead, scheduled for removal in v14")]
public IEnumerable<KeyValuePair<string, IEnumerable<object?>>> GetIndexValues(IProperty property, string? culture, string? segment, bool published, IEnumerable<string> availableCultures) => Array.Empty<KeyValuePair<string, IEnumerable<object?>>>();
[Obsolete("Use the overload with the availableCultures parameter instead, scheduled for removal in v14")]
@@ -1,6 +1,8 @@
using System.Xml.XPath;
using Microsoft.Extensions.DependencyInjection;
using Umbraco.Cms.Core.Models.PublishedContent;
using Umbraco.Cms.Core.Xml;
using Umbraco.Cms.Web.Common.DependencyInjection;
using Umbraco.Extensions;
namespace Umbraco.Cms.Core.PublishedCache;
@@ -9,10 +11,24 @@ public abstract class PublishedCacheBase : IPublishedCache
{
private readonly IVariationContextAccessor? _variationContextAccessor;
public PublishedCacheBase(IVariationContextAccessor variationContextAccessor) => _variationContextAccessor =
variationContextAccessor ?? throw new ArgumentNullException(nameof(variationContextAccessor));
protected PublishedCacheBase(bool previewDefault) => PreviewDefault = previewDefault;
[Obsolete("Use ctor with all parameters. This will be removed in V15")]
public PublishedCacheBase(IVariationContextAccessor variationContextAccessor)
: this(variationContextAccessor, false)
{
}
[Obsolete("Use ctor with all parameters. This will be removed in V15")]
protected PublishedCacheBase(bool previewDefault)
: this(StaticServiceProvider.Instance.GetRequiredService<IVariationContextAccessor>(), previewDefault)
{
}
public PublishedCacheBase(IVariationContextAccessor variationContextAccessor, bool previewDefault)
{
_variationContextAccessor = variationContextAccessor;
PreviewDefault = previewDefault;
}
public bool PreviewDefault { get; }
+24
View File
@@ -50,4 +50,28 @@ public class WebPath
return sb.ToString();
}
/// <summary>
/// Determines whether the provided web path is well-formed according to the specified UriKind.
/// </summary>
/// <param name="webPath">The web path to check. This can be null.</param>
/// <param name="uriKind">The kind of Uri (Absolute, Relative, or RelativeOrAbsolute).</param>
/// <returns>
/// true if <paramref name="webPath"/> is well-formed; otherwise, false.
/// </returns>
public static bool IsWellFormedWebPath(string? webPath, UriKind uriKind)
{
if (string.IsNullOrWhiteSpace(webPath))
{
return false;
}
if (webPath.StartsWith("//"))
{
return uriKind is not UriKind.Relative;
}
return Uri.IsWellFormedUriString(webPath, uriKind);
}
}
+23 -36
View File
@@ -167,12 +167,7 @@ public class ContentPermissions
throw new ArgumentNullException(nameof(user));
}
if (permissionsToCheck == null)
{
permissionsToCheck = Array.Empty<char>();
}
bool? hasPathAccess = null;
bool hasPathAccess;
entity = null;
if (nodeId == Constants.System.Root)
@@ -183,19 +178,17 @@ public class ContentPermissions
{
hasPathAccess = user.HasContentBinAccess(_entityService, _appCaches);
}
if (hasPathAccess.HasValue)
else
{
return hasPathAccess.Value ? ContentAccess.Granted : ContentAccess.Denied;
}
entity = _entityService.Get(nodeId, UmbracoObjectTypes.Document);
entity = _entityService.Get(nodeId, UmbracoObjectTypes.Document);
if (entity == null)
{
return ContentAccess.NotFound;
}
if (entity == null)
{
return ContentAccess.NotFound;
}
hasPathAccess = user.HasContentPathAccess(entity, _entityService, _appCaches);
hasPathAccess = user.HasContentPathAccess(entity, _entityService, _appCaches);
}
if (hasPathAccess == false)
{
@@ -208,7 +201,8 @@ public class ContentPermissions
}
// get the implicit/inherited permissions for the user for this path
return CheckPermissionsPath(entity.Path, user, permissionsToCheck)
// if there is no entity for this id, than just use the id as the path (i.e. -1 or -20)
return CheckPermissionsPath(entity?.Path ?? nodeId.ToString(), user, permissionsToCheck)
? ContentAccess.Granted
: ContentAccess.Denied;
}
@@ -235,12 +229,7 @@ public class ContentPermissions
throw new ArgumentNullException(nameof(user));
}
if (permissionsToCheck == null)
{
permissionsToCheck = Array.Empty<char>();
}
bool? hasPathAccess = null;
bool hasPathAccess;
contentItem = null;
if (nodeId == Constants.System.Root)
@@ -251,19 +240,17 @@ public class ContentPermissions
{
hasPathAccess = user.HasContentBinAccess(_entityService, _appCaches);
}
if (hasPathAccess.HasValue)
else
{
return hasPathAccess.Value ? ContentAccess.Granted : ContentAccess.Denied;
}
contentItem = _contentService.GetById(nodeId);
contentItem = _contentService.GetById(nodeId);
if (contentItem == null)
{
return ContentAccess.NotFound;
}
if (contentItem == null)
{
return ContentAccess.NotFound;
}
hasPathAccess = user.HasPathAccess(contentItem, _entityService, _appCaches);
hasPathAccess = user.HasPathAccess(contentItem, _entityService, _appCaches);
}
if (hasPathAccess == false)
{
@@ -276,7 +263,8 @@ public class ContentPermissions
}
// get the implicit/inherited permissions for the user for this path
return CheckPermissionsPath(contentItem.Path, user, permissionsToCheck)
// if there is no content item for this id, than just use the id as the path (i.e. -1 or -20)
return CheckPermissionsPath(contentItem?.Path ?? nodeId.ToString(), user, permissionsToCheck)
? ContentAccess.Granted
: ContentAccess.Denied;
}
@@ -288,8 +276,7 @@ public class ContentPermissions
permissionsToCheck = Array.Empty<char>();
}
// get the implicit/inherited permissions for the user for this path,
// if there is no content item for this id, than just use the id as the path (i.e. -1 or -20)
// get the implicit/inherited permissions for the user for this path
EntityPermissionSet permission = _userService.GetPermissionsForPath(user, path);
var allowed = true;
@@ -0,0 +1,14 @@
namespace Umbraco.Cms.Core.Security;
/// <summary>
/// Sanitizer service for the markdown editor.
/// </summary>
public interface IMarkdownSanitizer
{
/// <summary>
/// Sanitizes Markdown
/// </summary>
/// <param name="markdown">Markdown to be sanitized</param>
/// <returns>Sanitized Markdown</returns>
string Sanitize(string markdown);
}
@@ -0,0 +1,8 @@
namespace Umbraco.Cms.Core.Security;
/// <inheritdoc />
public class NoopMarkdownSanitizer : IMarkdownSanitizer
{
/// <inheritdoc />
public string Sanitize(string markdown) => markdown;
}
+13 -4
View File
@@ -372,7 +372,7 @@ public class ContentService : RepositoryService, IContentService
public IContent CreateAndSave(string name, int parentId, string contentTypeAlias, int userId = Constants.Security.SuperUserId)
{
// TODO: what about culture?
using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
using (ICoreScope scope = ScopeProvider.CreateCoreScope())
{
// locking the content tree secures content types too
scope.WriteLock(Constants.Locks.ContentTree);
@@ -395,6 +395,8 @@ public class ContentService : RepositoryService, IContentService
Save(content, userId);
scope.Complete();
return content;
}
}
@@ -416,7 +418,7 @@ public class ContentService : RepositoryService, IContentService
throw new ArgumentNullException(nameof(parent));
}
using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
using (ICoreScope scope = ScopeProvider.CreateCoreScope())
{
// locking the content tree secures content types too
scope.WriteLock(Constants.Locks.ContentTree);
@@ -431,6 +433,7 @@ public class ContentService : RepositoryService, IContentService
Save(content, userId);
scope.Complete();
return content;
}
}
@@ -508,10 +511,11 @@ public class ContentService : RepositoryService, IContentService
/// <inheritdoc />
public void PersistContentSchedule(IContent content, ContentScheduleCollection contentSchedule)
{
using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
using (ICoreScope scope = ScopeProvider.CreateCoreScope())
{
scope.WriteLock(Constants.Locks.ContentTree);
_documentRepository.PersistContentSchedule(content, contentSchedule);
scope.Complete();
}
}
@@ -2960,7 +2964,7 @@ public class ContentService : RepositoryService, IContentService
public ContentDataIntegrityReport CheckDataIntegrity(ContentDataIntegrityReportOptions options)
{
using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
using (ICoreScope scope = ScopeProvider.CreateCoreScope())
{
scope.WriteLock(Constants.Locks.ContentTree);
@@ -2973,6 +2977,8 @@ public class ContentService : RepositoryService, IContentService
scope.Notifications.Publish(new ContentTreeChangeNotification(root, TreeChangeTypes.RefreshAll, EventMessagesFactory.Get()));
}
scope.Complete();
return report;
}
}
@@ -3568,6 +3574,7 @@ public class ContentService : RepositoryService, IContentService
Audit(AuditType.Save, Constants.Security.SuperUserId, content.Id, $"Saved content template: {content.Name}");
scope.Notifications.Publish(new ContentSavedBlueprintNotification(content, evtMsgs));
scope.Notifications.Publish(new ContentTreeChangeNotification(content, TreeChangeTypes.RefreshNode, evtMsgs));
scope.Complete();
}
@@ -3582,6 +3589,7 @@ public class ContentService : RepositoryService, IContentService
scope.WriteLock(Constants.Locks.ContentTree);
_documentBlueprintRepository.Delete(content);
scope.Notifications.Publish(new ContentDeletedBlueprintNotification(content, evtMsgs));
scope.Notifications.Publish(new ContentTreeChangeNotification(content, TreeChangeTypes.Remove, evtMsgs));
scope.Complete();
}
}
@@ -3682,6 +3690,7 @@ public class ContentService : RepositoryService, IContentService
}
scope.Notifications.Publish(new ContentDeletedBlueprintNotification(blueprints, evtMsgs));
scope.Notifications.Publish(new ContentTreeChangeNotification(blueprints, TreeChangeTypes.Remove, evtMsgs));
scope.Complete();
}
}
@@ -322,7 +322,6 @@ public abstract class ContentTypeServiceBase<TRepository, TItem> : ContentTypeSe
}
using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
{
scope.ReadLock(ReadLockIds);
return Repository.GetMany(ids.ToArray());
@@ -68,7 +68,7 @@ internal class ContentVersionService : IContentVersionService
/// <inheritdoc />
public void SetPreventCleanup(int versionId, bool preventCleanup, int userId = -1)
{
using (ICoreScope scope = _scopeProvider.CreateCoreScope(autoComplete: true))
using (ICoreScope scope = _scopeProvider.CreateCoreScope())
{
scope.WriteLock(Constants.Locks.ContentTree);
_documentVersionRepository.SetPreventCleanup(versionId, preventCleanup);
@@ -77,6 +77,7 @@ internal class ContentVersionService : IContentVersionService
if (version is null)
{
scope.Complete();
return;
}
@@ -87,6 +88,7 @@ internal class ContentVersionService : IContentVersionService
var message = $"set preventCleanup = '{preventCleanup}' for version '{versionId}'";
Audit(auditType, userId, version.ContentId, message, $"{version.VersionDate}");
scope.Complete();
}
}
@@ -120,13 +122,14 @@ internal class ContentVersionService : IContentVersionService
*
* tl;dr lots of scopes to enable other connections to use the DB whilst we work.
*/
using (ICoreScope scope = _scopeProvider.CreateCoreScope(autoComplete: true))
using (ICoreScope scope = _scopeProvider.CreateCoreScope())
{
IReadOnlyCollection<ContentVersionMeta>? allHistoricVersions =
_documentVersionRepository.GetDocumentVersionsEligibleForCleanup();
if (allHistoricVersions is null)
{
scope.Complete();
return Array.Empty<ContentVersionMeta>();
}
@@ -149,6 +152,8 @@ internal class ContentVersionService : IContentVersionService
versionsToDelete.Add(version);
}
scope.Complete();
}
if (!versionsToDelete.Any())
@@ -161,7 +166,7 @@ internal class ContentVersionService : IContentVersionService
foreach (IEnumerable<ContentVersionMeta> group in versionsToDelete.InGroupsOf(Constants.Sql.MaxParameterCount))
{
using (ICoreScope scope = _scopeProvider.CreateCoreScope(autoComplete: true))
using (ICoreScope scope = _scopeProvider.CreateCoreScope())
{
scope.WriteLock(Constants.Locks.ContentTree);
var groupEnumerated = group.ToList();
@@ -174,12 +179,16 @@ internal class ContentVersionService : IContentVersionService
scope.Notifications.Publish(
new ContentDeletedVersionsNotification(version.ContentId, messages, version.VersionId));
}
scope.Complete();
}
}
using (_scopeProvider.CreateCoreScope(autoComplete: true))
using (ICoreScope scope = _scopeProvider.CreateCoreScope())
{
Audit(AuditType.Delete, Constants.Security.SuperUserId, -1, $"Removed {versionsToDelete.Count} ContentVersion(s) according to cleanup policy");
scope.Complete();
}
return versionsToDelete;
+1 -1
View File
@@ -608,7 +608,7 @@ namespace Umbraco.Cms.Core.Services.Implement
public IReadOnlyDictionary<Udi, IEnumerable<string>> GetReferences(int id)
{
using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete:true);
using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true);
return _dataTypeRepository.FindUsages(id);
}
@@ -33,7 +33,7 @@ public class DefaultContentVersionCleanupPolicy : IContentVersionCleanupPolicy
var theRest = new List<ContentVersionMeta>();
using (_scopeProvider.CreateCoreScope(autoComplete: true))
using (ICoreScope scope = _scopeProvider.CreateCoreScope())
{
var policyOverrides = _documentVersionRepository.GetCleanupPolicies()?
.ToDictionary(x => x.ContentTypeId);
@@ -77,6 +77,8 @@ public class DefaultContentVersionCleanupPolicy : IContentVersionCleanupPolicy
yield return version;
}
}
scope.Complete();
}
}
+2 -1
View File
@@ -1197,7 +1197,7 @@ namespace Umbraco.Cms.Core.Services
public ContentDataIntegrityReport CheckDataIntegrity(ContentDataIntegrityReportOptions options)
{
using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
using (ICoreScope scope = ScopeProvider.CreateCoreScope())
{
scope.WriteLock(Constants.Locks.MediaTree);
@@ -1210,6 +1210,7 @@ namespace Umbraco.Cms.Core.Services
scope.Notifications.Publish(new MediaTreeChangeNotification(root, TreeChangeTypes.RefreshAll, EventMessagesFactory.Get()));
}
scope.Complete();
return report;
}
}
@@ -59,8 +59,10 @@ public class TwoFactorLoginService : ITwoFactorLoginService2
/// <inheritdoc />
public async Task DeleteUserLoginsAsync(Guid userOrMemberKey)
{
using ICoreScope scope = _scopeProvider.CreateCoreScope(autoComplete: true);
using ICoreScope scope = _scopeProvider.CreateCoreScope();
await _twoFactorLoginRepository.DeleteUserLoginsAsync(userOrMemberKey);
scope.Complete();
}
/// <inheritdoc />
@@ -155,8 +157,12 @@ public class TwoFactorLoginService : ITwoFactorLoginService2
/// <inheritdoc />
public async Task<bool> DisableAsync(Guid userOrMemberKey, string providerName)
{
using ICoreScope scope = _scopeProvider.CreateCoreScope(autoComplete: true);
return await _twoFactorLoginRepository.DeleteUserLoginsAsync(userOrMemberKey, providerName);
using ICoreScope scope = _scopeProvider.CreateCoreScope();
var result = await _twoFactorLoginRepository.DeleteUserLoginsAsync(userOrMemberKey, providerName);
scope.Complete();
return result;
}
/// <inheritdoc />
@@ -173,9 +179,10 @@ public class TwoFactorLoginService : ITwoFactorLoginService2
/// <inheritdoc />
public Task SaveAsync(TwoFactorLogin twoFactorLogin)
{
using ICoreScope scope = _scopeProvider.CreateCoreScope(autoComplete: true);
using ICoreScope scope = _scopeProvider.CreateCoreScope();
_twoFactorLoginRepository.Save(twoFactorLogin);
scope.Complete();
return Task.CompletedTask;
}
@@ -8,6 +8,11 @@ namespace Umbraco.Extensions;
public static class UserServiceExtensions
{
public static EntityPermission? GetPermissions(this IUserService userService, IUser? user, string path)
{
return userService.GetAllPermissions(user, path).FirstOrDefault();
}
public static EntityPermissionCollection GetAllPermissions(this IUserService userService, IUser? user, string path)
{
var ids = path.Split(Constants.CharArrays.Comma, StringSplitOptions.RemoveEmptyEntries)
.Select(x =>
@@ -23,7 +28,7 @@ public static class UserServiceExtensions
" could not be parsed into an array of integers or the path was empty");
}
return userService.GetPermissions(user, ids[^1]).FirstOrDefault();
return userService.GetPermissions(user, ids[^1]);
}
/// <summary>
+118
View File
@@ -0,0 +1,118 @@
using System.Diagnostics;
namespace Umbraco.Cms.Core;
/// <summary>
/// Makes a code block timed (take at least a certain amount of time). This class cannot be inherited.
/// </summary>
public sealed class TimedScope : IDisposable, IAsyncDisposable
{
private readonly TimeSpan _duration;
private readonly CancellationTokenSource _cancellationTokenSource;
private readonly Stopwatch _stopwatch;
/// <summary>
/// Gets the elapsed time.
/// </summary>
/// <value>
/// The elapsed time.
/// </value>
public TimeSpan Elapsed => _stopwatch.Elapsed;
/// <summary>
/// Gets the remaining time.
/// </summary>
/// <value>
/// The remaining time.
/// </value>
public TimeSpan Remaining
=> TryGetRemaining(out TimeSpan remaining) ? remaining : TimeSpan.Zero;
/// <summary>
/// Initializes a new instance of the <see cref="TimedScope" /> class.
/// </summary>
/// <param name="millisecondsDuration">The number of milliseconds the scope should at least take.</param>
public TimedScope(long millisecondsDuration)
: this(TimeSpan.FromMilliseconds(millisecondsDuration))
{ }
/// <summary>
/// Initializes a new instance of the <see cref="TimedScope" /> class.
/// </summary>
/// <param name="millisecondsDuration">The number of milliseconds the scope should at least take.</param>
/// <param name="cancellationToken">The cancellation token.</param>
public TimedScope(long millisecondsDuration, CancellationToken cancellationToken)
: this(TimeSpan.FromMilliseconds(millisecondsDuration), cancellationToken)
{ }
/// <summary>
/// Initializes a new instance of the <see cref="TimedScope"/> class.
/// </summary>
/// <param name="duration">The duration the scope should at least take.</param>
public TimedScope(TimeSpan duration)
: this(duration, new CancellationTokenSource())
{ }
/// <summary>
/// Initializes a new instance of the <see cref="TimedScope" /> class.
/// </summary>
/// <param name="duration">The duration the scope should at least take.</param>
/// <param name="cancellationToken">The cancellation token.</param>
public TimedScope(TimeSpan duration, CancellationToken cancellationToken)
: this(duration, CancellationTokenSource.CreateLinkedTokenSource(cancellationToken))
{ }
private TimedScope(TimeSpan duration, CancellationTokenSource cancellationTokenSource)
{
_duration = duration;
_cancellationTokenSource = cancellationTokenSource;
_stopwatch = new Stopwatch();
_stopwatch.Start();
}
/// <summary>
/// Cancels the timed scope.
/// </summary>
public void Cancel()
=> _cancellationTokenSource.Cancel();
/// <summary>
/// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
/// </summary>
/// <remarks>
/// This will block using <see cref="Thread.Sleep(TimeSpan)" /> until the remaining time has elapsed, if not cancelled.
/// </remarks>
public void Dispose()
{
if (_cancellationTokenSource.IsCancellationRequested is false &&
TryGetRemaining(out TimeSpan remaining))
{
Thread.Sleep(remaining);
}
}
/// <summary>
/// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources asynchronously.
/// </summary>
/// <returns>
/// A task that represents the asynchronous dispose operation.
/// </returns>
/// <remarks>
/// This will delay using <see cref="Task.Delay(TimeSpan, CancellationToken)" /> until the remaining time has elapsed, if not cancelled.
/// </remarks>
public async ValueTask DisposeAsync()
{
if (_cancellationTokenSource.IsCancellationRequested is false &&
TryGetRemaining(out TimeSpan remaining))
{
await Task.Delay(remaining, _cancellationTokenSource.Token).ConfigureAwait(false);
}
}
private bool TryGetRemaining(out TimeSpan remaining)
{
remaining = _duration.Subtract(Elapsed);
return remaining > TimeSpan.Zero;
}
}
+16 -16
View File
@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<PackageId>Umbraco.Cms.Core</PackageId>
<Title>Umbraco CMS - Core</Title>
@@ -7,22 +7,22 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="6.0.0" />
<PackageReference Include="Microsoft.Extensions.FileProviders.Embedded" Version="6.0.5" />
<PackageReference Include="Microsoft.Extensions.FileProviders.Physical" Version="6.0.0" />
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="6.0.0" />
<PackageReference Include="Microsoft.Extensions.Identity.Core" Version="6.0.5" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="6.0.0" />
<PackageReference Include="Microsoft.Extensions.Options" Version="6.0.0" />
<PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="6.0.0" />
<PackageReference Include="Microsoft.Extensions.Options.DataAnnotations" Version="6.0.0" />
<PackageReference Include="System.ComponentModel.Annotations" Version="5.0.0" />
<PackageReference Include="System.Reflection.Emit.Lightweight" Version="4.7.0" />
<PackageReference Include="System.Runtime.Caching" Version="6.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" />
<PackageReference Include="Microsoft.Extensions.FileProviders.Embedded" />
<PackageReference Include="Microsoft.Extensions.FileProviders.Physical" />
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions"/>
<PackageReference Include="Microsoft.Extensions.Identity.Core" />
<PackageReference Include="Microsoft.Extensions.Logging" />
<PackageReference Include="Microsoft.Extensions.Options"/>
<PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" />
<PackageReference Include="Microsoft.Extensions.Options.DataAnnotations"/>
<PackageReference Include="System.ComponentModel.Annotations" />
<PackageReference Include="System.Reflection.Emit.Lightweight" />
<PackageReference Include="System.Runtime.Caching" />
<!-- 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 -->
<PackageReference Include="System.Net.Http" Version="4.3.4" />
<PackageReference Include="System.Security.Cryptography.Xml" Version="6.0.1" />
<PackageReference Include="System.Text.RegularExpressions" Version="4.3.1" />
<PackageReference Include="System.Net.Http" />
<PackageReference Include="System.Security.Cryptography.Xml" />
<PackageReference Include="System.Text.RegularExpressions" />
</ItemGroup>
<ItemGroup>
@@ -7,7 +7,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Examine" Version="3.0.1" />
<PackageReference Include="Examine" />
</ItemGroup>
<ItemGroup>
@@ -67,15 +67,6 @@ public class DistributedCacheBinder :
_distributedCache.RefreshContentCache(notification.Changes.ToArray());
}
// private void ContentService_SavedBlueprint(IContentService sender, SaveEventArgs<IContent> e)
// {
// _distributedCache.RefreshUnpublishedPageCache(e.SavedEntities.ToArray());
// }
// private void ContentService_DeletedBlueprint(IContentService sender, DeleteEventArgs<IContent> e)
// {
// _distributedCache.RemoveUnpublishedPageCache(e.DeletedEntities.ToArray());
// }
#endregion
#region LocalizationService / Dictionary
@@ -1,4 +1,4 @@
// Copyright (c) Umbraco.
// Copyright (c) Umbraco.
// See LICENSE for more details.
using Umbraco.Cms.Core.Cache;
@@ -132,7 +132,13 @@ public static class DistributedCacheExtensions
public static void RefreshAllContentCache(this DistributedCache dc)
{
ContentCacheRefresher.JsonPayload[] payloads = new[] { new ContentCacheRefresher.JsonPayload(0, null, TreeChangeTypes.RefreshAll) };
ContentCacheRefresher.JsonPayload[] payloads = new[]
{
new ContentCacheRefresher.JsonPayload()
{
ChangeTypes = TreeChangeTypes.RefreshAll
}
};
// note: refresh all content cache does refresh content types too
dc.RefreshByPayload(ContentCacheRefresher.UniqueId, payloads);
@@ -145,8 +151,13 @@ public static class DistributedCacheExtensions
return;
}
IEnumerable<ContentCacheRefresher.JsonPayload> payloads = changes
.Select(x => new ContentCacheRefresher.JsonPayload(x.Item.Id, x.Item.Key, x.ChangeTypes));
IEnumerable<ContentCacheRefresher.JsonPayload> payloads = changes.Select(x => new ContentCacheRefresher.JsonPayload()
{
Id = x.Item.Id,
Key = x.Item.Key,
ChangeTypes = x.ChangeTypes,
Blueprint = x.Item.Blueprint
});
dc.RefreshByPayload(ContentCacheRefresher.UniqueId, payloads);
}
@@ -358,10 +358,13 @@ public static partial class UmbracoBuilderExtensions
builder
.AddNotificationHandler<ContentSavingNotification, BlockListPropertyNotificationHandler>()
.AddNotificationHandler<ContentCopyingNotification, BlockListPropertyNotificationHandler>()
.AddNotificationHandler<ContentScaffoldedNotification, BlockListPropertyNotificationHandler>()
.AddNotificationHandler<ContentSavingNotification, BlockGridPropertyNotificationHandler>()
.AddNotificationHandler<ContentCopyingNotification, BlockGridPropertyNotificationHandler>()
.AddNotificationHandler<ContentScaffoldedNotification, BlockGridPropertyNotificationHandler>()
.AddNotificationHandler<ContentSavingNotification, NestedContentPropertyHandler>()
.AddNotificationHandler<ContentCopyingNotification, NestedContentPropertyHandler>()
.AddNotificationHandler<ContentScaffoldedNotification, NestedContentPropertyHandler>()
.AddNotificationHandler<ContentCopiedNotification, FileUploadPropertyEditor>()
.AddNotificationHandler<ContentDeletedNotification, FileUploadPropertyEditor>()
.AddNotificationHandler<MediaDeletedNotification, FileUploadPropertyEditor>()
@@ -1,6 +1,9 @@
using Examine;
using Microsoft.Extensions.DependencyInjection;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.PropertyEditors;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Web.Common.DependencyInjection;
using Umbraco.Extensions;
namespace Umbraco.Cms.Infrastructure.Examine;
@@ -24,9 +27,26 @@ public abstract class BaseValueSetBuilder<TContent> : IValueSetBuilder<TContent>
[Obsolete("Use the overload that specifies availableCultures, scheduled for removal in v14")]
protected void AddPropertyValue(IProperty property, string? culture, string? segment, IDictionary<string, IEnumerable<object?>>? values)
=> AddPropertyValue(property, culture, segment, values, Enumerable.Empty<string>());
=> AddPropertyValue(
property,
culture,
segment,
values,
Enumerable.Empty<string>(),
StaticServiceProvider.Instance.GetRequiredService<IContentTypeService>().GetAll().ToDictionary(x=>x.Key));
protected void AddPropertyValue(IProperty property, string? culture, string? segment, IDictionary<string, IEnumerable<object?>>? values, IEnumerable<string> availableCultures)
[Obsolete("Use the overload that specifies availableCultures, scheduled for removal in v14")]
protected void AddPropertyValue(IProperty property, string? culture, string? segment,
IDictionary<string, IEnumerable<object?>>? values, IEnumerable<string> availableCultures)
=> AddPropertyValue(
property,
culture,
segment,
values,
Enumerable.Empty<string>(),
StaticServiceProvider.Instance.GetRequiredService<IContentTypeService>().GetAll().ToDictionary(x=>x.Key));
protected void AddPropertyValue(IProperty property, string? culture, string? segment, IDictionary<string, IEnumerable<object?>>? values, IEnumerable<string> availableCultures, IDictionary<Guid, IContentType> contentTypeDictionary)
{
IDataEditor? editor = _propertyEditors[property.PropertyType.PropertyEditorAlias];
if (editor == null)
@@ -35,7 +55,7 @@ public abstract class BaseValueSetBuilder<TContent> : IValueSetBuilder<TContent>
}
IEnumerable<KeyValuePair<string, IEnumerable<object?>>> indexVals =
editor.PropertyIndexValueFactory.GetIndexValues(property, culture, segment, PublishedValuesOnly, availableCultures);
editor.PropertyIndexValueFactory.GetIndexValues(property, culture, segment, PublishedValuesOnly, availableCultures, contentTypeDictionary);
foreach (KeyValuePair<string, IEnumerable<object?>> keyVal in indexVals)
{
if (keyVal.Key.IsNullOrWhiteSpace())
@@ -21,13 +21,34 @@ public class ContentValueSetBuilder : BaseValueSetBuilder<IContent>, IContentVal
private static readonly object[] NoValue = new[] { "n" };
private static readonly object[] YesValue = new[] { "y" };
private readonly IScopeProvider _scopeProvider;
private readonly ICoreScopeProvider _scopeProvider;
private readonly IShortStringHelper _shortStringHelper;
private readonly UrlSegmentProviderCollection _urlSegmentProviders;
private readonly IUserService _userService;
private readonly ILocalizationService _localizationService;
private readonly IContentTypeService _contentTypeService;
public ContentValueSetBuilder(
PropertyEditorCollection propertyEditors,
UrlSegmentProviderCollection urlSegmentProviders,
IUserService userService,
IShortStringHelper shortStringHelper,
ICoreScopeProvider scopeProvider,
bool publishedValuesOnly,
ILocalizationService localizationService,
IContentTypeService contentTypeService)
: base(propertyEditors, publishedValuesOnly)
{
_urlSegmentProviders = urlSegmentProviders;
_userService = userService;
_shortStringHelper = shortStringHelper;
_scopeProvider = scopeProvider;
_localizationService = localizationService;
_contentTypeService = contentTypeService;
}
[Obsolete("Use non-obsolete ctor, scheduled for removal in v14")]
public ContentValueSetBuilder(
PropertyEditorCollection propertyEditors,
UrlSegmentProviderCollection urlSegmentProviders,
@@ -36,16 +57,20 @@ public class ContentValueSetBuilder : BaseValueSetBuilder<IContent>, IContentVal
IScopeProvider scopeProvider,
bool publishedValuesOnly,
ILocalizationService localizationService)
: base(propertyEditors, publishedValuesOnly)
: this(
propertyEditors,
urlSegmentProviders,
userService,
shortStringHelper,
scopeProvider,
publishedValuesOnly,
localizationService,
StaticServiceProvider.Instance.GetRequiredService<IContentTypeService>())
{
_urlSegmentProviders = urlSegmentProviders;
_userService = userService;
_shortStringHelper = shortStringHelper;
_scopeProvider = scopeProvider;
_localizationService = localizationService;
}
[Obsolete("Use the constructor that takes an ILocalizationService, scheduled for removal in v14")]
[Obsolete("Use non-obsolete ctor, scheduled for removal in v14")]
public ContentValueSetBuilder(
PropertyEditorCollection propertyEditors,
UrlSegmentProviderCollection urlSegmentProviders,
@@ -60,7 +85,8 @@ public class ContentValueSetBuilder : BaseValueSetBuilder<IContent>, IContentVal
shortStringHelper,
scopeProvider,
publishedValuesOnly,
StaticServiceProvider.Instance.GetRequiredService<ILocalizationService>())
StaticServiceProvider.Instance.GetRequiredService<ILocalizationService>(),
StaticServiceProvider.Instance.GetRequiredService<IContentTypeService>())
{
}
@@ -72,7 +98,7 @@ public class ContentValueSetBuilder : BaseValueSetBuilder<IContent>, IContentVal
// We can lookup all of the creator/writer names at once which can save some
// processing below instead of one by one.
using (IScope scope = _scopeProvider.CreateScope())
using (ICoreScope scope = _scopeProvider.CreateCoreScope())
{
creatorIds = _userService.GetProfilesById(content.Select(x => x.CreatorId).ToArray())
.ToDictionary(x => x.Id, x => x);
@@ -86,6 +112,8 @@ public class ContentValueSetBuilder : BaseValueSetBuilder<IContent>, IContentVal
private IEnumerable<ValueSet> GetValueSetsEnumerable(IContent[] content, Dictionary<int, IProfile> creatorIds, Dictionary<int, IProfile> writerIds)
{
IDictionary<Guid, IContentType> contentTypeDictionary = _contentTypeService.GetAll().ToDictionary(x => x.Key);
// TODO: There is a lot of boxing going on here and ultimately all values will be boxed by Lucene anyways
// but I wonder if there's a way to reduce the boxing that we have to do or if it will matter in the end since
// Lucene will do it no matter what? One idea was to create a `FieldValue` struct which would contain `object`, `object[]`, `ValueType` and `ValueType[]`
@@ -162,13 +190,13 @@ public class ContentValueSetBuilder : BaseValueSetBuilder<IContent>, IContentVal
{
if (!property.PropertyType.VariesByCulture())
{
AddPropertyValue(property, null, null, values, availableCultures);
AddPropertyValue(property, null, null, values, availableCultures, contentTypeDictionary);
}
else
{
foreach (var culture in c.AvailableCultures)
{
AddPropertyValue(property, culture.ToLowerInvariant(), null, values, availableCultures);
AddPropertyValue(property, culture.ToLowerInvariant(), null, values, availableCultures, contentTypeDictionary);
}
}
}
@@ -66,7 +66,17 @@ public class ExamineIndexRebuilder : IIndexRebuilder
_logger.LogInformation("Starting async background thread for rebuilding index {indexName}.", indexName);
_backgroundTaskQueue.QueueBackgroundWorkItem(
cancellationToken => Task.Run(() => RebuildIndex(indexName, delay.Value, cancellationToken)));
cancellationToken =>
{
// Do not flow AsyncLocal to the child thread
using (ExecutionContext.SuppressFlow())
{
Task.Run(() => RebuildIndex(indexName, delay.Value, cancellationToken));
// immediately return so the queue isn't waiting.
return Task.CompletedTask;
}
});
}
else
{
@@ -93,12 +103,16 @@ public class ExamineIndexRebuilder : IIndexRebuilder
_backgroundTaskQueue.QueueBackgroundWorkItem(
cancellationToken =>
{
// This is a fire/forget task spawned by the background thread queue (which means we
// don't need to worry about ExecutionContext flowing).
Task.Run(() => RebuildIndexes(onlyEmptyIndexes, delay.Value, cancellationToken));
// Do not flow AsyncLocal to the child thread
using (ExecutionContext.SuppressFlow())
{
// This is a fire/forget task spawned by the background thread queue (which means we
// don't need to worry about ExecutionContext flowing).
Task.Run(() => RebuildIndexes(onlyEmptyIndexes, delay.Value, cancellationToken));
// immediately return so the queue isn't waiting.
return Task.CompletedTask;
// immediately return so the queue isn't waiting.
return Task.CompletedTask;
}
});
}
else
@@ -1,10 +1,12 @@
using Examine;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using Umbraco.Cms.Core.Configuration.Models;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.PropertyEditors;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Strings;
using Umbraco.Cms.Web.Common.DependencyInjection;
using Umbraco.Extensions;
namespace Umbraco.Cms.Infrastructure.Examine;
@@ -14,6 +16,7 @@ public class MediaValueSetBuilder : BaseValueSetBuilder<IMedia>
private readonly ContentSettings _contentSettings;
private readonly MediaUrlGeneratorCollection _mediaUrlGenerators;
private readonly IShortStringHelper _shortStringHelper;
private readonly IContentTypeService _contentTypeService;
private readonly UrlSegmentProviderCollection _urlSegmentProviders;
private readonly IUserService _userService;
@@ -23,19 +26,41 @@ public class MediaValueSetBuilder : BaseValueSetBuilder<IMedia>
MediaUrlGeneratorCollection mediaUrlGenerators,
IUserService userService,
IShortStringHelper shortStringHelper,
IOptions<ContentSettings> contentSettings)
IOptions<ContentSettings> contentSettings,
IContentTypeService contentTypeService)
: base(propertyEditors, false)
{
_urlSegmentProviders = urlSegmentProviders;
_mediaUrlGenerators = mediaUrlGenerators;
_userService = userService;
_shortStringHelper = shortStringHelper;
_contentTypeService = contentTypeService;
_contentSettings = contentSettings.Value;
}
[Obsolete("Use non-obsolete ctor, scheduled for removal in v14")]
public MediaValueSetBuilder(
PropertyEditorCollection propertyEditors,
UrlSegmentProviderCollection urlSegmentProviders,
MediaUrlGeneratorCollection mediaUrlGenerators,
IUserService userService,
IShortStringHelper shortStringHelper,
IOptions<ContentSettings> contentSettings)
: this(propertyEditors,
urlSegmentProviders,
mediaUrlGenerators,
userService,
shortStringHelper,
contentSettings,
StaticServiceProvider.Instance.GetRequiredService<IContentTypeService>())
{
}
/// <inheritdoc />
public override IEnumerable<ValueSet> GetValueSets(params IMedia[] media)
{
IDictionary<Guid, IContentType> contentTypeDictionary = _contentTypeService.GetAll().ToDictionary(x => x.Key);
foreach (IMedia m in media)
{
var urlValue = m.GetUrlSegment(_shortStringHelper, _urlSegmentProviders);
@@ -65,7 +90,7 @@ public class MediaValueSetBuilder : BaseValueSetBuilder<IMedia>
foreach (IProperty property in m.Properties)
{
AddPropertyValue(property, null, null, values, m.AvailableCultures);
AddPropertyValue(property, null, null, values, m.AvailableCultures, contentTypeDictionary);
}
var vs = new ValueSet(m.Id.ToInvariantString(), IndexTypes.Media, m.ContentType.Alias, values);
@@ -1,20 +1,34 @@
using Examine;
using Microsoft.Extensions.DependencyInjection;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.PropertyEditors;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Web.Common.DependencyInjection;
using Umbraco.Extensions;
namespace Umbraco.Cms.Infrastructure.Examine;
public class MemberValueSetBuilder : BaseValueSetBuilder<IMember>
{
public MemberValueSetBuilder(PropertyEditorCollection propertyEditors)
private readonly IContentTypeService _contentTypeService;
public MemberValueSetBuilder(PropertyEditorCollection propertyEditors, IContentTypeService contentTypeService)
: base(propertyEditors, false)
{
_contentTypeService = contentTypeService;
}
[Obsolete("Use non-obsolete ctor, scheduled for removal in v14")]
public MemberValueSetBuilder(PropertyEditorCollection propertyEditors)
: this(propertyEditors, StaticServiceProvider.Instance.GetRequiredService<IContentTypeService>())
{
}
/// <inheritdoc />
public override IEnumerable<ValueSet> GetValueSets(params IMember[] members)
{
IDictionary<Guid, IContentType> contentTypeDictionary = _contentTypeService.GetAll().ToDictionary(x => x.Key);
foreach (IMember m in members)
{
var values = new Dictionary<string, IEnumerable<object?>>
@@ -37,7 +51,7 @@ public class MemberValueSetBuilder : BaseValueSetBuilder<IMember>
foreach (IProperty property in m.Properties)
{
AddPropertyValue(property, null, null, values, m.AvailableCultures);
AddPropertyValue(property, null, null, values, m.AvailableCultures, contentTypeDictionary);
}
var vs = new ValueSet(m.Id.ToInvariantString(), IndexTypes.Member, m.ContentType.Alias, values);
@@ -94,7 +94,7 @@ public class ScheduledPublishing : RecurringHostedServiceBase
// but then what should be its "scope"? could we attach it to scopes?
// - and we should definitively *not* have to flush it here (should be auto)
using UmbracoContextReference contextReference = _umbracoContextFactory.EnsureUmbracoContext();
using ICoreScope scope = _scopeProvider.CreateCoreScope(autoComplete: true);
using ICoreScope scope = _scopeProvider.CreateCoreScope();
/* We used to assume that there will never be two instances running concurrently where (IsMainDom && ServerRole == SchedulingPublisher)
* However this is possible during an azure deployment slot swap for the SchedulingPublisher instance when trying to achieve zero downtime deployments.
@@ -113,6 +113,8 @@ public class ScheduledPublishing : RecurringHostedServiceBase
grouped.Count(),
grouped.Key);
}
scope.Complete();
}
finally
{
@@ -67,7 +67,7 @@ public class ThreadAbortExceptionEnricher : ILogEventEnricher
private void DumpThreadAborts(LogEvent logEvent, ILogEventPropertyFactory propertyFactory)
{
if (!IsTimeoutThreadAbortException(logEvent.Exception))
if (logEvent.Exception is null || !IsTimeoutThreadAbortException(logEvent.Exception))
{
return;
}
@@ -1,6 +1,9 @@
using Umbraco.Cms.Core.Models;
using Microsoft.Extensions.DependencyInjection;
using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Persistence.Repositories;
using Umbraco.Cms.Core.Scoping;
using Umbraco.Cms.Infrastructure.Scoping;
using Umbraco.Cms.Web.Common.DependencyInjection;
using IScope = Umbraco.Cms.Infrastructure.Scoping.IScope;
namespace Umbraco.Cms.Core.Logging.Viewer;
@@ -10,6 +13,21 @@ public class LogViewerConfig : ILogViewerConfig
private readonly ILogViewerQueryRepository _logViewerQueryRepository;
private readonly IScopeProvider _scopeProvider;
[Obsolete("Use non-obsolete ctor. This will be removed in Umbraco 14.")]
public LogViewerConfig(ILogViewerQueryRepository logViewerQueryRepository, Umbraco.Cms.Core.Scoping.IScopeProvider scopeProvider)
: this(logViewerQueryRepository, StaticServiceProvider.Instance.GetRequiredService<IScopeProvider>())
{
}
//Temp ctor used by MSDI (Greedy)
[Obsolete("Use non-obsolete ctor. This will be removed in Umbraco 14.")]
public LogViewerConfig(ILogViewerQueryRepository logViewerQueryRepository, Umbraco.Cms.Core.Scoping.IScopeProvider coreScopeProvider, IScopeProvider scopeProvider)
: this(logViewerQueryRepository, scopeProvider)
{
}
public LogViewerConfig(ILogViewerQueryRepository logViewerQueryRepository, IScopeProvider scopeProvider)
{
_logViewerQueryRepository = logViewerQueryRepository;
@@ -26,15 +44,16 @@ public class LogViewerConfig : ILogViewerConfig
public IReadOnlyList<SavedLogSearch>? AddSavedSearch(string? name, string? query)
{
using IScope scope = _scopeProvider.CreateScope(autoComplete: true);
using IScope scope = _scopeProvider.CreateScope();
_logViewerQueryRepository.Save(new LogViewerQuery(name, query));
scope.Complete();
return GetSavedSearches();
}
public IReadOnlyList<SavedLogSearch>? DeleteSavedSearch(string? name, string? query)
{
using IScope scope = _scopeProvider.CreateScope(autoComplete: true);
using IScope scope = _scopeProvider.CreateScope();
ILogViewerQuery? item = name is null ? null : _logViewerQueryRepository.GetByName(name);
if (item is not null)
{
@@ -42,6 +61,8 @@ public class LogViewerConfig : ILogViewerConfig
}
// Return the updated object - so we can instantly reset the entire array from the API response
return GetSavedSearches();
IReadOnlyList<SavedLogSearch> result = GetSavedSearches()!;
scope.Complete();
return result;
}
}
@@ -250,6 +250,11 @@ public class ManifestParser : IManifestParser
return Array.Empty<string>();
}
return Directory.GetFiles(_path, "package.manifest", SearchOption.AllDirectories);
var files = Directory.GetFiles(_path, "package.manifest", SearchOption.AllDirectories);
// Ensure a consistent, alphabetical sorting of paths, because this is not guaranteed to be the same between file systems or OSes
Array.Sort(files);
return files;
}
}
@@ -37,5 +37,5 @@ internal class AccessDto
[ResultColumn]
[Reference(ReferenceType.Many, ReferenceMemberName = "AccessId")]
public List<AccessRuleDto> Rules { get; set; } = null!;
public List<AccessRuleDto> Rules { get; set; } = new();
}
@@ -1,3 +1,4 @@
using System.Data.Common;
using NPoco;
using Umbraco.Cms.Infrastructure.Migrations.Install;
@@ -33,4 +34,7 @@ public interface IUmbracoDatabase : IDatabase
bool IsUmbracoInstalled();
DatabaseSchemaResult ValidateSchema();
/// <returns>The number of rows affected.</returns>
int ExecuteNonQuery(DbCommand command) => command.ExecuteNonQuery();
}
@@ -1082,82 +1082,78 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement
protected void PersistRelations(TEntity entity)
{
// Get all references from our core built in DataEditors/Property Editors
// Along with seeing if deverlopers want to collect additional references from the DataValueReferenceFactories collection
var trackedRelations = new List<UmbracoEntityReference>();
trackedRelations.AddRange(_dataValueReferenceFactories.GetAllReferences(entity.Properties, PropertyEditors));
// Get all references and automatic relation type aliases
ISet<UmbracoEntityReference> references = _dataValueReferenceFactories.GetAllReferences(entity.Properties, PropertyEditors);
ISet<string> automaticRelationTypeAliases = _dataValueReferenceFactories.GetAllAutomaticRelationTypesAliases(PropertyEditors);
var relationTypeAliases = GetAutomaticRelationTypesAliases(entity.Properties, PropertyEditors).ToArray();
// First delete all auto-relations for this entity
RelationRepository.DeleteByParent(entity.Id, relationTypeAliases);
if (trackedRelations.Count == 0)
if (references.Count == 0)
{
// Delete all relations using the automatic relation type aliases
RelationRepository.DeleteByParent(entity.Id, automaticRelationTypeAliases.ToArray());
// No need to add new references/relations
return;
}
trackedRelations = trackedRelations.Distinct().ToList();
var udiToGuids = trackedRelations.Select(x => x.Udi as GuidUdi)
.ToDictionary(x => (Udi)x!, x => x!.Guid);
// Lookup all relation type IDs
var relationTypeLookup = RelationTypeRepository.GetMany(Array.Empty<int>())
.Where(x => automaticRelationTypeAliases.Contains(x.Alias))
.ToDictionary(x => x.Alias, x => x.Id);
// lookup in the DB all INT ids for the GUIDs and chuck into a dictionary
var keyToIds = Database.Fetch<NodeIdKey>(Sql()
.Select<NodeDto>(x => x.NodeId, x => x.UniqueId)
.From<NodeDto>()
.WhereIn<NodeDto>(x => x.UniqueId, udiToGuids.Values))
.ToDictionary(x => x.UniqueId, x => x.NodeId);
var allRelationTypes = RelationTypeRepository.GetMany(Array.Empty<int>())?
.ToDictionary(x => x.Alias, x => x);
IEnumerable<ReadOnlyRelation> toSave = trackedRelations.Select(rel =>
{
if (allRelationTypes is null || !allRelationTypes.TryGetValue(rel.RelationTypeAlias, out IRelationType? relationType))
{
throw new InvalidOperationException($"The relation type {rel.RelationTypeAlias} does not exist");
}
if (!udiToGuids.TryGetValue(rel.Udi, out Guid guid))
{
return null; // This shouldn't happen!
}
if (!keyToIds.TryGetValue(guid, out var id))
{
return null; // This shouldn't happen!
}
return new ReadOnlyRelation(entity.Id, id, relationType.Id);
}).WhereNotNull();
// Save bulk relations
RelationRepository.SaveBulk(toSave);
}
private IEnumerable<string> GetAutomaticRelationTypesAliases(
IPropertyCollection properties,
PropertyEditorCollection propertyEditors)
{
var automaticRelationTypesAliases = new HashSet<string>(Constants.Conventions.RelationTypes.AutomaticRelationTypes);
foreach (IProperty property in properties)
// Lookup node IDs for all GUID based UDIs
IEnumerable<Guid> keys = references.Select(x => x.Udi).OfType<GuidUdi>().Select(x => x.Guid);
var keysLookup = Database.FetchByGroups<NodeIdKey, Guid>(keys, Constants.Sql.MaxParameterCount, guids =>
{
if (propertyEditors.TryGet(property.PropertyType.PropertyEditorAlias, out IDataEditor? editor) is false )
{
continue;
}
return Sql()
.Select<NodeDto>(x => x.NodeId, x => x.UniqueId)
.From<NodeDto>()
.WhereIn<NodeDto>(x => x.UniqueId, guids);
}).ToDictionary(x => x.UniqueId, x => x.NodeId);
if (editor.GetValueEditor() is IDataValueReference reference)
// Get all valid relations
var relations = new List<(int ChildId, int RelationTypeId)>(references.Count);
foreach (UmbracoEntityReference reference in references)
{
if (string.IsNullOrEmpty(reference.RelationTypeAlias))
{
foreach (var alias in reference.GetAutomaticRelationTypesAliases())
{
automaticRelationTypesAliases.Add(alias);
}
// Reference does not specify a relation type alias, so skip adding a relation
Logger.LogDebug("The reference to {Udi} does not specify a relation type alias, so it will not be saved as relation.", reference.Udi);
}
else if (!automaticRelationTypeAliases.Contains(reference.RelationTypeAlias))
{
// Returning a reference that doesn't use an automatic relation type is an issue that should be fixed in code
Logger.LogError("The reference to {Udi} uses a relation type {RelationTypeAlias} that is not an automatic relation type.", reference.Udi, reference.RelationTypeAlias);
}
else if (!relationTypeLookup.TryGetValue(reference.RelationTypeAlias, out int relationTypeId))
{
// A non-existent relation type could be caused by an environment issue (e.g. it was manually removed)
Logger.LogWarning("The reference to {Udi} uses a relation type {RelationTypeAlias} that does not exist.", reference.Udi, reference.RelationTypeAlias);
}
else if (reference.Udi is not GuidUdi udi || !keysLookup.TryGetValue(udi.Guid, out var id))
{
// Relations only support references to items that are stored in the NodeDto table (because of foreign key constraints)
Logger.LogInformation("The reference to {Udi} can not be saved as relation, because doesn't have a node ID.", reference.Udi);
}
else
{
relations.Add((id, relationTypeId));
}
}
return automaticRelationTypesAliases;
// Get all existing relations (optimize for adding new and keeping existing relations)
var query = Query<IRelation>().Where(x => x.ParentId == entity.Id).WhereIn(x => x.RelationTypeId, relationTypeLookup.Values);
var existingRelations = RelationRepository.GetPagedRelationsByQuery(query, 0, int.MaxValue, out _, null)
.ToDictionary(x => (x.ChildId, x.RelationTypeId)); // Relations are unique by parent ID, child ID and relation type ID
// Add relations that don't exist yet
var relationsToAdd = relations.Except(existingRelations.Keys).Select(x => new ReadOnlyRelation(entity.Id, x.ChildId, x.RelationTypeId));
RelationRepository.SaveBulk(relationsToAdd);
// Delete relations that don't exist anymore
foreach (IRelation relation in existingRelations.Where(x => !relations.Contains(x.Key)).Select(x => x.Value))
{
RelationRepository.Delete(relation);
}
}
/// <summary>
@@ -1223,8 +1223,11 @@ AND umbracoNode.id <> @id",
/// If this is not done, then in some cases the "edited" value for a particular culture for a document will remain true
/// when it should be false
/// if the property was changed to invariant. In order to do this we need to recalculate this value based on the values
/// stored for each
/// property, culture and current/published version.
/// stored for each property, culture and current/published version.
///
/// Some of the sql statements in this function have a tendency to take a lot of parameters (nodeIds)
/// as the WhereIn Npoco method translates all the nodeIds being passed in as parameters when using the SqlClient provider.
/// this results in to many parameters (>2100) error => We need to batch the calls
/// </remarks>
private void RenormalizeDocumentEditedFlags(
IReadOnlyCollection<int> propertyTypeIds,
@@ -1380,15 +1383,23 @@ AND umbracoNode.id <> @id",
// Now bulk update the table DocumentCultureVariationDto, once for edited = true, another for edited = false
foreach (IGrouping<bool, DocumentCultureVariationDto> editValue in toUpdate.GroupBy(x => x.Edited))
{
Database.Execute(Sql().Update<DocumentCultureVariationDto>(u => u.Set(x => x.Edited, editValue.Key))
.WhereIn<DocumentCultureVariationDto>(x => x.Id, editValue.Select(x => x.Id)));
// update in batches to account for maximum parameter count
foreach (IEnumerable<DocumentCultureVariationDto> batchedValues in editValue.InGroupsOf(Constants.Sql.MaxParameterCount))
{
Database.Execute(Sql().Update<DocumentCultureVariationDto>(u => u.Set(x => x.Edited, editValue.Key))
.WhereIn<DocumentCultureVariationDto>(x => x.Id, batchedValues.Select(x => x.Id)));
}
}
// Now bulk update the umbracoDocument table
foreach (IGrouping<bool, KeyValuePair<int, bool>> editValue in editedDocument.GroupBy(x => x.Value))
foreach (IGrouping<bool, KeyValuePair<int, bool>> groupByValue in editedDocument.GroupBy(x => x.Value))
{
Database.Execute(Sql().Update<DocumentDto>(u => u.Set(x => x.Edited, editValue.Key))
.WhereIn<DocumentDto>(x => x.NodeId, editValue.Select(x => x.Key)));
// update in batches to account for maximum parameter count
foreach (IEnumerable<KeyValuePair<int, bool>> batch in groupByValue.InGroupsOf(Constants.Sql.MaxParameterCount))
{
Database.Execute(Sql().Update<DocumentDto>(u => u.Set(x => x.Edited, groupByValue.Key))
.WhereIn<DocumentDto>(x => x.NodeId, batch.Select(x => x.Key)));
}
}
}
@@ -264,9 +264,16 @@ public class CreatedPackageSchemaRepository : ICreatedPackagesRepository
_hostingEnvironment.MapPathContentRoot(Path.Combine(
_createdPackagesFolderPath,
definition.Name.Replace(' ', '_')));
var expectedRoot = Path.GetFullPath(_hostingEnvironment.MapPathContentRoot(_createdPackagesFolderPath));
var finalPackagePath = Path.GetFullPath(Path.Combine(directoryName, fileName));
if (finalPackagePath.StartsWith(expectedRoot) == false)
{
throw new IOException("Invalid path due to the package name");
}
Directory.CreateDirectory(directoryName);
var finalPackagePath = Path.Combine(directoryName, fileName);
// Clean existing files
foreach (var packagePath in new[] { definition.PackagePath, finalPackagePath })
@@ -224,6 +224,14 @@ public class UmbracoDatabase : Database, IUmbracoDatabase
return databaseSchemaValidationResult ?? new DatabaseSchemaResult();
}
public int ExecuteNonQuery(DbCommand command)
{
OnExecutingCommand(command);
var i = command.ExecuteNonQuery();
OnExecutedCommand(command);
return i;
}
/// <summary>
/// Returns true if Umbraco database tables are detected to be installed
/// </summary>
@@ -17,12 +17,14 @@ internal abstract class BlockEditorPropertyValueEditor : DataValueEditor, IDataV
{
private BlockEditorValues? _blockEditorValues;
private readonly IDataTypeService _dataTypeService;
private readonly ILogger<BlockEditorPropertyValueEditor> _logger;
private readonly PropertyEditorCollection _propertyEditors;
private readonly DataValueReferenceFactoryCollection _dataValueReferenceFactories;
private readonly ILogger<BlockEditorPropertyValueEditor> _logger;
protected BlockEditorPropertyValueEditor(
DataEditorAttribute attribute,
PropertyEditorCollection propertyEditors,
DataValueReferenceFactoryCollection dataValueReferenceFactories,
IDataTypeService dataTypeService,
ILocalizedTextService textService,
ILogger<BlockEditorPropertyValueEditor> logger,
@@ -32,6 +34,7 @@ internal abstract class BlockEditorPropertyValueEditor : DataValueEditor, IDataV
: base(textService, shortStringHelper, jsonSerializer, ioHelper, attribute)
{
_propertyEditors = propertyEditors;
_dataValueReferenceFactories = dataValueReferenceFactories;
_dataTypeService = dataTypeService;
_logger = logger;
}
@@ -42,73 +45,60 @@ internal abstract class BlockEditorPropertyValueEditor : DataValueEditor, IDataV
set => _blockEditorValues = value;
}
/// <inheritdoc />
public IEnumerable<UmbracoEntityReference> GetReferences(object? value)
{
var rawJson = value == null ? string.Empty : value is string str ? str : value.ToString();
var result = new List<UmbracoEntityReference>();
BlockEditorData? blockEditorData = BlockEditorValues.DeserializeAndClean(rawJson);
if (blockEditorData == null)
// Group by property editor alias to avoid duplicate lookups and optimize value parsing
foreach (var valuesByPropertyEditorAlias in GetAllPropertyValues(value).GroupBy(x => x.PropertyType.PropertyEditorAlias, x => x.Value))
{
return Enumerable.Empty<UmbracoEntityReference>();
}
// loop through all content and settings data
foreach (BlockItemData row in blockEditorData.BlockValue.ContentData.Concat(blockEditorData.BlockValue.SettingsData))
{
foreach (KeyValuePair<string, BlockItemData.BlockPropertyValue> prop in row.PropertyValues)
if (!_propertyEditors.TryGet(valuesByPropertyEditorAlias.Key, out IDataEditor? dataEditor))
{
IDataEditor? propEditor = _propertyEditors[prop.Value.PropertyType.PropertyEditorAlias];
continue;
}
IDataValueEditor? valueEditor = propEditor?.GetValueEditor();
if (!(valueEditor is IDataValueReference reference))
{
continue;
}
var val = prop.Value.Value?.ToString();
IEnumerable<UmbracoEntityReference> refs = reference.GetReferences(val);
result.AddRange(refs);
// Use distinct values to avoid duplicate parsing of the same value
foreach (UmbracoEntityReference reference in _dataValueReferenceFactories.GetReferences(dataEditor, valuesByPropertyEditorAlias.Distinct()))
{
yield return reference;
}
}
return result;
}
/// <inheritdoc />
public IEnumerable<ITag> GetTags(object? value, object? dataTypeConfiguration, int? languageId)
{
foreach (BlockItemData.BlockPropertyValue propertyValue in GetAllPropertyValues(value))
{
if (!_propertyEditors.TryGet(propertyValue.PropertyType.PropertyEditorAlias, out IDataEditor? dataEditor) ||
dataEditor.GetValueEditor() is not IDataValueTags dataValueTags)
{
continue;
}
object? configuration = _dataTypeService.GetDataType(propertyValue.PropertyType.DataTypeKey)?.Configuration;
foreach (ITag tag in dataValueTags.GetTags(propertyValue.Value, configuration, languageId))
{
yield return tag;
}
}
}
private IEnumerable<BlockItemData.BlockPropertyValue> GetAllPropertyValues(object? value)
{
var rawJson = value == null ? string.Empty : value is string str ? str : value.ToString();
BlockEditorData? blockEditorData = BlockEditorValues.DeserializeAndClean(rawJson);
if (blockEditorData == null)
if (blockEditorData is null)
{
return Enumerable.Empty<ITag>();
yield break;
}
var result = new List<ITag>();
// loop through all content and settings data
foreach (BlockItemData row in blockEditorData.BlockValue.ContentData.Concat(blockEditorData.BlockValue.SettingsData))
// Return all property values from the content and settings data
IEnumerable<BlockItemData> data = blockEditorData.BlockValue.ContentData.Concat(blockEditorData.BlockValue.SettingsData);
foreach (BlockItemData.BlockPropertyValue propertyValue in data.SelectMany(x => x.PropertyValues.Select(x => x.Value)))
{
foreach (KeyValuePair<string, BlockItemData.BlockPropertyValue> prop in row.PropertyValues)
{
IDataEditor? propEditor = _propertyEditors[prop.Value.PropertyType.PropertyEditorAlias];
IDataValueEditor? valueEditor = propEditor?.GetValueEditor();
if (valueEditor is not IDataValueTags tagsProvider)
{
continue;
}
object? configuration = _dataTypeService.GetDataType(prop.Value.PropertyType.DataTypeKey)?.Configuration;
result.AddRange(tagsProvider.GetTags(prop.Value.Value, configuration, languageId));
}
yield return propertyValue;
}
return result;
}
#region Convert database // editor
@@ -119,7 +109,6 @@ internal abstract class BlockEditorPropertyValueEditor : DataValueEditor, IDataV
/// Ensure that sub-editor values are translated through their ToEditor methods
/// </summary>
/// <param name="property"></param>
/// <param name="dataTypeService"></param>
/// <param name="culture"></param>
/// <param name="segment"></param>
/// <returns></returns>
@@ -50,6 +50,7 @@ public abstract class BlockGridPropertyEditorBase : DataEditor
public BlockGridEditorPropertyValueEditor(
DataEditorAttribute attribute,
PropertyEditorCollection propertyEditors,
DataValueReferenceFactoryCollection dataValueReferenceFactories,
IDataTypeService dataTypeService,
ILocalizedTextService textService,
ILogger<BlockEditorPropertyValueEditor> logger,
@@ -58,7 +59,7 @@ public abstract class BlockGridPropertyEditorBase : DataEditor
IIOHelper ioHelper,
IContentTypeService contentTypeService,
IPropertyValidationService propertyValidationService)
: base(attribute, propertyEditors, dataTypeService, textService, logger, shortStringHelper, jsonSerializer, ioHelper)
: base(attribute, propertyEditors, dataValueReferenceFactories, dataTypeService, textService, logger, shortStringHelper, jsonSerializer, ioHelper)
{
BlockEditorValues = new BlockEditorValues(new BlockGridEditorDataConverter(jsonSerializer), contentTypeService, logger);
Validators.Add(new BlockEditorValidator(propertyValidationService, BlockEditorValues, contentTypeService));
@@ -46,6 +46,7 @@ public abstract class BlockListPropertyEditorBase : DataEditor
public BlockListEditorPropertyValueEditor(
DataEditorAttribute attribute,
PropertyEditorCollection propertyEditors,
DataValueReferenceFactoryCollection dataValueReferenceFactories,
IDataTypeService dataTypeService,
IContentTypeService contentTypeService,
ILocalizedTextService textService,
@@ -54,7 +55,7 @@ public abstract class BlockListPropertyEditorBase : DataEditor
IJsonSerializer jsonSerializer,
IIOHelper ioHelper,
IPropertyValidationService propertyValidationService) :
base(attribute, propertyEditors, dataTypeService, textService, logger, shortStringHelper, jsonSerializer, ioHelper)
base(attribute, propertyEditors, dataValueReferenceFactories,dataTypeService, textService, logger, shortStringHelper, jsonSerializer, ioHelper)
{
BlockEditorValues = new BlockEditorValues(new BlockListEditorDataConverter(), contentTypeService, logger);
Validators.Add(new BlockEditorValidator(propertyValidationService, BlockEditorValues, contentTypeService));
@@ -38,10 +38,13 @@ internal sealed class BlockValuePropertyIndexValueFactory :
_contentTypeService = contentTypeService;
}
[Obsolete("Use non-obsolete overload, scheduled for removal in v14")]
protected override IContentType? GetContentTypeOfNestedItem(BlockItemData input) =>
_contentTypeService.Get(input.ContentTypeKey);
protected override IContentType? GetContentTypeOfNestedItem(BlockItemData input, IDictionary<Guid, IContentType> contentTypeDictionary)
=> contentTypeDictionary.TryGetValue(input.ContentTypeKey, out var result) ? result : null;
protected override IDictionary<string, object?> GetRawProperty(BlockItemData blockItemData) =>
blockItemData.RawPropertyValues;
@@ -10,9 +10,16 @@ using Umbraco.Extensions;
namespace Umbraco.Cms.Core.PropertyEditors;
/// <summary>
/// Handles nested Udi keys when
/// - saving: Empty keys get generated
/// - copy: keys get replaced by new ones while keeping references intact
/// - scaffolding: keys get replaced by new ones while keeping references intact
/// </summary>
public abstract class ComplexPropertyEditorContentNotificationHandler :
INotificationHandler<ContentSavingNotification>,
INotificationHandler<ContentCopyingNotification>
INotificationHandler<ContentCopyingNotification>,
INotificationHandler<ContentScaffoldedNotification>
{
protected abstract string EditorAlias { get; }
@@ -31,6 +38,12 @@ public abstract class ComplexPropertyEditorContentNotificationHandler :
}
}
public void Handle(ContentScaffoldedNotification notification)
{
IEnumerable<IProperty> props = notification.Scaffold.GetPropertiesByEditor(EditorAlias);
UpdatePropertyValues(props, false);
}
protected abstract string FormatPropertyValue(string rawJson, bool onlyMissingKeys);
private void UpdatePropertyValues(IEnumerable<IProperty> props, bool onlyMissingKeys)
@@ -2,19 +2,20 @@
// See LICENSE for more details.
using System.Runtime.Serialization;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using Umbraco.Cms.Core.IO;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.ContentEditing;
using Umbraco.Cms.Core.Models.Editors;
using Umbraco.Cms.Core.Models.Entities;
using Umbraco.Cms.Core.Models.PublishedContent;
using Umbraco.Cms.Core.PublishedCache;
using Umbraco.Cms.Core.Routing;
using Umbraco.Cms.Core.Serialization;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Strings;
using Umbraco.Extensions;
using Umbraco.Cms.Web.Common.DependencyInjection;
namespace Umbraco.Cms.Core.PropertyEditors;
@@ -26,11 +27,57 @@ public class MultiUrlPickerValueEditor : DataValueEditor, IDataValueReference
NullValueHandling = NullValueHandling.Ignore,
};
private readonly IEntityService _entityService;
private readonly ILogger<MultiUrlPickerValueEditor> _logger;
private readonly IPublishedSnapshotAccessor _publishedSnapshotAccessor;
private readonly IPublishedUrlProvider _publishedUrlProvider;
private readonly IContentService _contentService;
private readonly IMediaService _mediaService;
public MultiUrlPickerValueEditor(
ILogger<MultiUrlPickerValueEditor> logger,
ILocalizedTextService localizedTextService,
IShortStringHelper shortStringHelper,
DataEditorAttribute attribute,
IPublishedUrlProvider publishedUrlProvider,
IJsonSerializer jsonSerializer,
IIOHelper ioHelper,
IContentService contentService,
IMediaService mediaService)
: base(localizedTextService, shortStringHelper, jsonSerializer, ioHelper, attribute)
{
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
_publishedUrlProvider = publishedUrlProvider;
_contentService = contentService;
_mediaService = mediaService;
}
[Obsolete("Use non-obsolete constructor. Scheduled for removal in Umbraco 14.")]
public MultiUrlPickerValueEditor(
IEntityService entityService,
IPublishedSnapshotAccessor publishedSnapshotAccessor,
ILogger<MultiUrlPickerValueEditor> logger,
ILocalizedTextService localizedTextService,
IShortStringHelper shortStringHelper,
DataEditorAttribute attribute,
IPublishedUrlProvider publishedUrlProvider,
IJsonSerializer jsonSerializer,
IIOHelper ioHelper,
IContentService contentService,
IMediaService mediaService)
:this(
logger,
localizedTextService,
shortStringHelper,
attribute,
publishedUrlProvider,
jsonSerializer,
ioHelper,
contentService,
mediaService)
{
}
[Obsolete("Use non-obsolete constructor. Scheduled for removal in Umbraco 14.")]
public MultiUrlPickerValueEditor(
IEntityService entityService,
IPublishedSnapshotAccessor publishedSnapshotAccessor,
@@ -41,13 +88,18 @@ public class MultiUrlPickerValueEditor : DataValueEditor, IDataValueReference
IPublishedUrlProvider publishedUrlProvider,
IJsonSerializer jsonSerializer,
IIOHelper ioHelper)
: base(localizedTextService, shortStringHelper, jsonSerializer, ioHelper, attribute)
: this(
logger,
localizedTextService,
shortStringHelper,
attribute,
publishedUrlProvider,
jsonSerializer,
ioHelper,
StaticServiceProvider.Instance.GetRequiredService<IContentService>(),
StaticServiceProvider.Instance.GetRequiredService<IMediaService>())
{
_entityService = entityService ?? throw new ArgumentNullException(nameof(entityService));
_publishedSnapshotAccessor = publishedSnapshotAccessor ??
throw new ArgumentNullException(nameof(publishedSnapshotAccessor));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
_publishedUrlProvider = publishedUrlProvider;
}
public IEnumerable<UmbracoEntityReference> GetReferences(object? value)
@@ -86,26 +138,6 @@ public class MultiUrlPickerValueEditor : DataValueEditor, IDataValueReference
{
List<LinkDto>? links = JsonConvert.DeserializeObject<List<LinkDto>>(value);
List<LinkDto>? documentLinks = links?.FindAll(link =>
link.Udi != null && link.Udi.EntityType == Constants.UdiEntityType.Document);
List<LinkDto>? mediaLinks = links?.FindAll(link =>
link.Udi != null && link.Udi.EntityType == Constants.UdiEntityType.Media);
var entities = new List<IEntitySlim>();
if (documentLinks?.Count > 0)
{
entities.AddRange(
_entityService.GetAll(
UmbracoObjectTypes.Document,
documentLinks.Select(link => link.Udi!.Guid).ToArray()));
}
if (mediaLinks?.Count > 0)
{
entities.AddRange(
_entityService.GetAll(UmbracoObjectTypes.Media, mediaLinks.Select(link => link.Udi!.Guid).ToArray()));
}
var result = new List<LinkDisplay>();
if (links is null)
{
@@ -114,7 +146,7 @@ public class MultiUrlPickerValueEditor : DataValueEditor, IDataValueReference
foreach (LinkDto dto in links)
{
GuidUdi? udi = null;
GuidUdi? udi = dto.Udi;
var icon = "icon-link";
var published = true;
var trashed = false;
@@ -122,35 +154,30 @@ public class MultiUrlPickerValueEditor : DataValueEditor, IDataValueReference
if (dto.Udi != null)
{
IUmbracoEntity? entity = entities.Find(e => e.Key == dto.Udi.Guid);
if (entity == null)
if (dto.Udi.EntityType == Constants.UdiEntityType.Document)
{
continue;
}
url = _publishedUrlProvider.GetUrl(dto.Udi.Guid, UrlMode.Relative, culture);
IContent? c = _contentService.GetById(dto.Udi.Guid);
IPublishedSnapshot publishedSnapshot = _publishedSnapshotAccessor.GetRequiredPublishedSnapshot();
if (entity is IDocumentEntitySlim documentEntity)
{
icon = documentEntity.ContentTypeIcon;
published = culture == null
? documentEntity.Published
: documentEntity.PublishedCultures.Contains(culture);
udi = new GuidUdi(Constants.UdiEntityType.Document, documentEntity.Key);
url = publishedSnapshot.Content?.GetById(entity.Key)?.Url(_publishedUrlProvider) ?? "#";
trashed = documentEntity.Trashed;
if (c is not null)
{
published = culture == null
? c.Published
: c.PublishedCultures.Contains(culture);
icon = c.ContentType.Icon;
trashed = c.Trashed;
}
}
else if (entity is IContentEntitySlim contentEntity)
else if (dto.Udi.EntityType == Constants.UdiEntityType.Media)
{
icon = contentEntity.ContentTypeIcon;
published = !contentEntity.Trashed;
udi = new GuidUdi(Constants.UdiEntityType.Media, contentEntity.Key);
url = publishedSnapshot.Media?.GetById(entity.Key)?.Url(_publishedUrlProvider) ?? "#";
trashed = contentEntity.Trashed;
}
else
{
// Not supported
continue;
url = _publishedUrlProvider.GetMediaUrl(dto.Udi.Guid, UrlMode.Relative, culture);
IMedia? m = _mediaService.GetById(dto.Udi.Guid);
if (m is not null)
{
published = m.Trashed is false;
icon = m.ContentType.Icon;
trashed = m.Trashed;
}
}
}
@@ -89,9 +89,10 @@ public class NestedContentPropertyEditor : DataEditor
internal class NestedContentPropertyValueEditor : DataValueEditor, IDataValueReference, IDataValueTags
{
private readonly IDataTypeService _dataTypeService;
private readonly PropertyEditorCollection _propertyEditors;
private readonly DataValueReferenceFactoryCollection _dataValueReferenceFactories;
private readonly ILogger<NestedContentPropertyEditor> _logger;
private readonly NestedContentValues _nestedContentValues;
private readonly PropertyEditorCollection _propertyEditors;
public NestedContentPropertyValueEditor(
IDataTypeService dataTypeService,
@@ -100,16 +101,19 @@ public class NestedContentPropertyEditor : DataEditor
IShortStringHelper shortStringHelper,
DataEditorAttribute attribute,
PropertyEditorCollection propertyEditors,
DataValueReferenceFactoryCollection dataValueReferenceFactories,
ILogger<NestedContentPropertyEditor> logger,
IJsonSerializer jsonSerializer,
IIOHelper ioHelper,
IPropertyValidationService propertyValidationService)
: base(localizedTextService, shortStringHelper, jsonSerializer, ioHelper, attribute)
{
_propertyEditors = propertyEditors;
_dataTypeService = dataTypeService;
_propertyEditors = propertyEditors;
_dataValueReferenceFactories = dataValueReferenceFactories;
_logger = logger;
_nestedContentValues = new NestedContentValues(contentTypeService);
Validators.Add(new NestedContentValidator(propertyValidationService, _nestedContentValues, contentTypeService));
}
@@ -137,66 +141,47 @@ public class NestedContentPropertyEditor : DataEditor
}
}
/// <inheritdoc />
public IEnumerable<UmbracoEntityReference> GetReferences(object? value)
{
var rawJson = value == null ? string.Empty : value is string str ? str : value.ToString();
var result = new List<UmbracoEntityReference>();
foreach (NestedContentValues.NestedContentRowValue row in _nestedContentValues.GetPropertyValues(rawJson))
// Group by property editor alias to avoid duplicate lookups and optimize value parsing
foreach (var valuesByPropertyEditorAlias in GetAllPropertyValues(value).GroupBy(x => x.PropertyType.PropertyEditorAlias, x => x.Value))
{
foreach (KeyValuePair<string, NestedContentValues.NestedContentPropertyValue> prop in
row.PropertyValues)
if (!_propertyEditors.TryGet(valuesByPropertyEditorAlias.Key, out IDataEditor? dataEditor))
{
IDataEditor? propEditor = _propertyEditors[prop.Value.PropertyType.PropertyEditorAlias];
continue;
}
IDataValueEditor? valueEditor = propEditor?.GetValueEditor();
if (!(valueEditor is IDataValueReference reference))
{
continue;
}
var val = prop.Value.Value?.ToString();
IEnumerable<UmbracoEntityReference> refs = reference.GetReferences(val);
result.AddRange(refs);
// Use distinct values to avoid duplicate parsing of the same value
foreach (UmbracoEntityReference reference in _dataValueReferenceFactories.GetReferences(dataEditor, valuesByPropertyEditorAlias.Distinct()))
{
yield return reference;
}
}
return result;
}
/// <inheritdoc />
public IEnumerable<ITag> GetTags(object? value, object? dataTypeConfiguration, int? languageId)
{
IReadOnlyList<NestedContentValues.NestedContentRowValue> rows =
_nestedContentValues.GetPropertyValues(value);
var result = new List<ITag>();
foreach (NestedContentValues.NestedContentRowValue row in rows.ToList())
foreach (NestedContentValues.NestedContentPropertyValue propertyValue in GetAllPropertyValues(value))
{
foreach (KeyValuePair<string, NestedContentValues.NestedContentPropertyValue> prop in row.PropertyValues
.ToList())
if (!_propertyEditors.TryGet(propertyValue.PropertyType.PropertyEditorAlias, out IDataEditor? dataEditor) ||
dataEditor.GetValueEditor() is not IDataValueTags dataValueTags)
{
IDataEditor? propEditor = _propertyEditors[prop.Value.PropertyType.PropertyEditorAlias];
continue;
}
IDataValueEditor? valueEditor = propEditor?.GetValueEditor();
if (valueEditor is not IDataValueTags tagsProvider)
{
continue;
}
object? configuration = _dataTypeService.GetDataType(prop.Value.PropertyType.DataTypeKey)?.Configuration;
result.AddRange(tagsProvider.GetTags(prop.Value.Value, configuration, languageId));
object? configuration = _dataTypeService.GetDataType(propertyValue.PropertyType.DataTypeKey)?.Configuration;
foreach (ITag tag in dataValueTags.GetTags(propertyValue.Value, configuration, languageId))
{
yield return tag;
}
}
return result;
}
private IEnumerable<NestedContentValues.NestedContentPropertyValue> GetAllPropertyValues(object? value)
=> _nestedContentValues.GetPropertyValues(value).SelectMany(x => x.PropertyValues.Values);
#region DB to String
public override string ConvertDbToString(IPropertyType propertyType, object? propertyValue)
@@ -422,7 +407,8 @@ public class NestedContentPropertyEditor : DataEditor
// set values to null
row.PropertyValues[elementTypeProp.Alias] = new NestedContentValues.NestedContentPropertyValue
{
PropertyType = elementTypeProp, Value = null,
PropertyType = elementTypeProp,
Value = null,
};
row.RawPropertyValues[elementTypeProp.Alias] = null;
}
@@ -39,6 +39,11 @@ internal sealed class NestedContentPropertyIndexValueFactory
_contentTypeService = contentTypeService;
}
protected override IContentType? GetContentTypeOfNestedItem(
NestedContentPropertyEditor.NestedContentValues.NestedContentRowValue input, IDictionary<Guid, IContentType> contentTypeDictionary)
=> contentTypeDictionary.Values.FirstOrDefault(x=>x.Alias.Equals(input.ContentTypeAlias));
[Obsolete("Use non-obsolete overload, scheduled for removal in v14")]
protected override IContentType? GetContentTypeOfNestedItem(
NestedContentPropertyEditor.NestedContentValues.NestedContentRowValue input)
=> _contentTypeService.Get(input.ContentTypeAlias);
@@ -4,6 +4,7 @@ using Microsoft.Extensions.Options;
using Umbraco.Cms.Core.Configuration.Models;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Serialization;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Infrastructure.Examine;
using Umbraco.Cms.Web.Common.DependencyInjection;
using Umbraco.Extensions;
@@ -42,20 +43,39 @@ internal abstract class NestedPropertyIndexValueFactoryBase<TSerialized, TItem>
bool published) =>
Handle(deserializedPropertyValue, property, culture, segment, published, Enumerable.Empty<string>());
[Obsolete("Use the overload that specifies availableCultures, scheduled for removal in v14")]
protected override IEnumerable<KeyValuePair<string, IEnumerable<object?>>> Handle(
TSerialized deserializedPropertyValue,
IProperty property,
string? culture,
string? segment,
bool published,
IEnumerable<string> availableCultures)
IEnumerable<string> availableCultures) =>
Handle(
deserializedPropertyValue,
property,
culture,
segment,
published,
Enumerable.Empty<string>(),
StaticServiceProvider.Instance.GetRequiredService<IContentTypeService>().GetAll().ToDictionary(x=>x.Key));
protected override IEnumerable<KeyValuePair<string, IEnumerable<object?>>> Handle(
TSerialized deserializedPropertyValue,
IProperty property,
string? culture,
string? segment,
bool published,
IEnumerable<string> availableCultures,
IDictionary<Guid, IContentType> contentTypeDictionary)
{
var result = new List<KeyValuePair<string, IEnumerable<object?>>>();
var index = 0;
foreach (TItem nestedContentRowValue in GetDataItems(deserializedPropertyValue))
{
IContentType? contentType = GetContentTypeOfNestedItem(nestedContentRowValue);
IContentType? contentType = GetContentTypeOfNestedItem(nestedContentRowValue, contentTypeDictionary);
if (contentType is null)
{
@@ -125,6 +145,9 @@ internal abstract class NestedPropertyIndexValueFactoryBase<TSerialized, TItem>
/// <summary>
/// Gets the content type using the nested item.
/// </summary>
protected abstract IContentType? GetContentTypeOfNestedItem(TItem nestedItem, IDictionary<Guid, IContentType> contentTypeDictionary);
[Obsolete("Use non-obsolete overload. Scheduled for removal in Umbraco 14.")]
protected abstract IContentType? GetContentTypeOfNestedItem(TItem nestedItem);
/// <summary>
+12 -14
View File
@@ -23,10 +23,9 @@ namespace Umbraco.Cms.Infrastructure.Scoping
private readonly bool _autoComplete;
private readonly CoreDebugSettings _coreDebugSettings;
private readonly object _dictionaryLocker;
private readonly IEventAggregator _eventAggregator;
private readonly IsolationLevel _isolationLevel;
private readonly object _lockQueueLocker = new();
private readonly object _locker = new();
private readonly ILogger<Scope> _logger;
private readonly MediaFileManager _mediaFileManager;
private readonly RepositoryCacheMode _repositoryCacheMode;
@@ -87,7 +86,6 @@ namespace Umbraco.Cms.Infrastructure.Scoping
_scopeFileSystem = scopeFileSystems;
_autoComplete = autoComplete;
Detachable = detachable;
_dictionaryLocker = new object();
#if DEBUG_SCOPES
_scopeProvider.RegisterScope(this);
@@ -562,7 +560,7 @@ namespace Umbraco.Cms.Infrastructure.Scoping
DisposeLastScope();
}
lock (_lockQueueLocker)
lock (_locker)
{
_queuedLocks?.Clear();
}
@@ -573,24 +571,24 @@ namespace Umbraco.Cms.Infrastructure.Scoping
public void EagerReadLock(params int[] lockIds) => EagerReadLockInner(InstanceId, null, lockIds);
/// <inheritdoc />
public void ReadLock(params int[] lockIds) => LazyReadLockInner(InstanceId, lockIds);
public void ReadLock(params int[] lockIds) => EagerReadLockInner(InstanceId, null, lockIds);
public void EagerReadLock(TimeSpan timeout, int lockId) =>
EagerReadLockInner(InstanceId, timeout, lockId);
/// <inheritdoc />
public void ReadLock(TimeSpan timeout, int lockId) => LazyReadLockInner(InstanceId, timeout, lockId);
public void ReadLock(TimeSpan timeout, int lockId) => EagerReadLockInner(InstanceId, timeout, lockId);
public void EagerWriteLock(params int[] lockIds) => EagerWriteLockInner(InstanceId, null, lockIds);
/// <inheritdoc />
public void WriteLock(params int[] lockIds) => LazyWriteLockInner(InstanceId, lockIds);
public void WriteLock(params int[] lockIds) => EagerWriteLockInner(InstanceId, null, lockIds);
public void EagerWriteLock(TimeSpan timeout, int lockId) =>
EagerWriteLockInner(InstanceId, timeout, lockId);
/// <inheritdoc />
public void WriteLock(TimeSpan timeout, int lockId) => LazyWriteLockInner(InstanceId, timeout, lockId);
public void WriteLock(TimeSpan timeout, int lockId) => EagerWriteLockInner(InstanceId, timeout, lockId);
/// <summary>
/// Used for testing. Ensures and gets any queued read locks.
@@ -659,7 +657,7 @@ namespace Umbraco.Cms.Infrastructure.Scoping
}
else
{
lock (_lockQueueLocker)
lock (_locker)
{
if (_queuedLocks?.Count > 0)
{
@@ -970,7 +968,7 @@ namespace Umbraco.Cms.Infrastructure.Scoping
}
else
{
lock (_dictionaryLocker)
lock (_locker)
{
_readLocksDictionary?.Remove(instanceId);
_writeLocksDictionary?.Remove(instanceId);
@@ -1045,7 +1043,7 @@ namespace Umbraco.Cms.Infrastructure.Scoping
private void LazyLockInner(DistributedLockType lockType, Guid instanceId, params int[] lockIds)
{
lock (_lockQueueLocker)
lock (_locker)
{
if (_queuedLocks == null)
{
@@ -1061,7 +1059,7 @@ namespace Umbraco.Cms.Infrastructure.Scoping
private void LazyLockInner(DistributedLockType lockType, Guid instanceId, TimeSpan timeout, int lockId)
{
lock (_lockQueueLocker)
lock (_locker)
{
if (_queuedLocks == null)
{
@@ -1088,7 +1086,7 @@ namespace Umbraco.Cms.Infrastructure.Scoping
}
else
{
lock (_dictionaryLocker)
lock (_locker)
{
foreach (var lockId in lockIds)
{
@@ -1122,7 +1120,7 @@ namespace Umbraco.Cms.Infrastructure.Scoping
}
else
{
lock (_dictionaryLocker)
lock (_locker)
{
foreach (var lockId in lockIds)
{
@@ -51,13 +51,15 @@ public sealed class ContentIndexingNotificationHandler : INotificationHandler<Co
foreach (ContentCacheRefresher.JsonPayload payload in (ContentCacheRefresher.JsonPayload[])args.MessageObject)
{
if (payload.Blueprint)
{
// Skip blueprints
continue;
}
if (payload.ChangeTypes.HasType(TreeChangeTypes.Remove))
{
if (deleteBatch == null)
{
deleteBatch = new HashSet<int>();
}
deleteBatch ??= new HashSet<int>();
deleteBatch.Add(payload.Id);
}
else if (payload.ChangeTypes.HasType(TreeChangeTypes.RefreshAll))
@@ -96,7 +96,7 @@ public class MemberUserStore : UmbracoUserStore<MemberIdentityUser, UmbracoIdent
throw new ArgumentNullException(nameof(user));
}
using ICoreScope scope = _scopeProvider.CreateCoreScope(autoComplete: true);
using ICoreScope scope = _scopeProvider.CreateCoreScope();
// create member
IMember memberEntity = _memberService.CreateMember(
@@ -150,6 +150,7 @@ public class MemberUserStore : UmbracoUserStore<MemberIdentityUser, UmbracoIdent
x.Value)));
}
scope.Complete();
return Task.FromResult(IdentityResult.Success);
}
catch (Exception ex)
@@ -179,7 +180,7 @@ public class MemberUserStore : UmbracoUserStore<MemberIdentityUser, UmbracoIdent
throw new InvalidOperationException("The user id must be an integer to work with the Umbraco");
}
using ICoreScope scope = _scopeProvider.CreateCoreScope(autoComplete: true);
using ICoreScope scope = _scopeProvider.CreateCoreScope();
IMember? found = _memberService.GetById(asInt);
if (found != null)
@@ -220,6 +221,7 @@ public class MemberUserStore : UmbracoUserStore<MemberIdentityUser, UmbracoIdent
}
}
scope.Complete();
return Task.FromResult(IdentityResult.Success);
}
catch (Exception ex)
@@ -134,8 +134,8 @@ public abstract class UmbracoUserManager<TUser, TPasswordConfig> : UserManager<T
/// <inheritdoc />
public override async Task<bool> CheckPasswordAsync(TUser user, string? password)
{
// we cannot proceed if the user passed in does not have an identity
if (user.HasIdentity == false)
// we cannot proceed if the user passed in does not have an identity, or if no password is provided.
if (user.HasIdentity == false || password is null)
{
return false;
}
@@ -252,7 +252,7 @@ public abstract class UmbracoUserManager<TUser, TPasswordConfig> : UserManager<T
public async Task<bool> ValidateCredentialsAsync(string username, string password)
{
TUser user = await FindByNameAsync(username);
if (user == null)
{
return false;
@@ -263,7 +263,7 @@ public abstract class UmbracoUserManager<TUser, TPasswordConfig> : UserManager<T
throw new NotSupportedException("The current user store does not implement " +
typeof(IUserPasswordStore<>));
}
var result = await VerifyPasswordAsync(userPasswordStore, user, password);
return result == PasswordVerificationResult.Success || result == PasswordVerificationResult.SuccessRehashNeeded;
@@ -1,3 +1,4 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using Umbraco.Cms.Core;
@@ -6,6 +7,7 @@ using Umbraco.Cms.Core.Models.Editors;
using Umbraco.Cms.Core.PropertyEditors;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Infrastructure.Macros;
using Umbraco.Cms.Web.Common.DependencyInjection;
using Umbraco.Extensions;
namespace Umbraco.Cms.Infrastructure.Templates;
@@ -15,12 +17,23 @@ public sealed class HtmlMacroParameterParser : IHtmlMacroParameterParser
private readonly ILogger<HtmlMacroParameterParser> _logger;
private readonly IMacroService _macroService;
private readonly ParameterEditorCollection _parameterEditors;
private readonly DataValueReferenceFactoryCollection _dataValueReferenceFactories;
[Obsolete("Use the non-obsolete overload instead, scheduled for removal in v14")]
public HtmlMacroParameterParser(IMacroService macroService, ILogger<HtmlMacroParameterParser> logger, ParameterEditorCollection parameterEditors)
: this(
macroService,
logger,
parameterEditors,
StaticServiceProvider.Instance.GetRequiredService<DataValueReferenceFactoryCollection>())
{ }
public HtmlMacroParameterParser(IMacroService macroService, ILogger<HtmlMacroParameterParser> logger, ParameterEditorCollection parameterEditors, DataValueReferenceFactoryCollection dataValueReferenceFactories)
{
_macroService = macroService;
_logger = logger;
_parameterEditors = parameterEditors;
_dataValueReferenceFactories = dataValueReferenceFactories;
}
/// <summary>
@@ -41,6 +54,7 @@ public sealed class HtmlMacroParameterParser : IHtmlMacroParameterParser
(macroAlias, macroAttributes) => foundMacros.Add(new Tuple<string?, Dictionary<string, string>>(
macroAlias,
new Dictionary<string, string>(macroAttributes, StringComparer.OrdinalIgnoreCase))));
foreach (UmbracoEntityReference umbracoEntityReference in GetUmbracoEntityReferencesFromMacros(foundMacros))
{
yield return umbracoEntityReference;
@@ -52,8 +66,7 @@ public sealed class HtmlMacroParameterParser : IHtmlMacroParameterParser
/// </summary>
/// <param name="macroGridControls"></param>
/// <returns></returns>
public IEnumerable<UmbracoEntityReference> FindUmbracoEntityReferencesFromGridControlMacros(
IEnumerable<GridValue.GridControl> macroGridControls)
public IEnumerable<UmbracoEntityReference> FindUmbracoEntityReferencesFromGridControlMacros(IEnumerable<GridValue.GridControl> macroGridControls)
{
var foundMacros = new List<Tuple<string?, Dictionary<string, string>>>();
@@ -65,8 +78,7 @@ public sealed class HtmlMacroParameterParser : IHtmlMacroParameterParser
// Collect any macro parameters that contain the media udi format
if (gridMacro is not null && gridMacro.MacroParameters is not null && gridMacro.MacroParameters.Any())
{
foundMacros.Add(
new Tuple<string?, Dictionary<string, string>>(gridMacro.MacroAlias, gridMacro.MacroParameters));
foundMacros.Add(new Tuple<string?, Dictionary<string, string>>(gridMacro.MacroAlias, gridMacro.MacroParameters));
}
}
@@ -101,14 +113,12 @@ public sealed class HtmlMacroParameterParser : IHtmlMacroParameterParser
continue;
}
foundMacroUmbracoEntityReferences.Add(
new UmbracoEntityReference(Udi.Create(Constants.UdiEntityType.Macro, macroConfig.Key)));
foundMacroUmbracoEntityReferences.Add(new UmbracoEntityReference(Udi.Create(Constants.UdiEntityType.Macro, macroConfig.Key)));
// Only do this if the macros actually have parameters
if (macroConfig.Properties.Keys.Any(f => f != "macroAlias"))
{
foreach (UmbracoEntityReference umbracoEntityReference in GetUmbracoEntityReferencesFromMacroParameters(
macro.Item2, macroConfig, _parameterEditors))
foreach (UmbracoEntityReference umbracoEntityReference in GetUmbracoEntityReferencesFromMacroParameters(macro.Item2, macroConfig, _parameterEditors))
{
yield return umbracoEntityReference;
}
@@ -130,41 +140,23 @@ public sealed class HtmlMacroParameterParser : IHtmlMacroParameterParser
/// look up the corresponding property editor for a macro parameter
/// </param>
/// <returns></returns>
private IEnumerable<UmbracoEntityReference> GetUmbracoEntityReferencesFromMacroParameters(
Dictionary<string, string> macroParameters, IMacro macroConfig, ParameterEditorCollection parameterEditors)
private IEnumerable<UmbracoEntityReference> GetUmbracoEntityReferencesFromMacroParameters(Dictionary<string, string> macroParameters, IMacro macroConfig, ParameterEditorCollection parameterEditors)
{
var foundUmbracoEntityReferences = new List<UmbracoEntityReference>();
foreach (IMacroProperty parameter in macroConfig.Properties)
{
if (macroParameters.TryGetValue(parameter.Alias, out var parameterValue))
{
var parameterEditorAlias = parameter.EditorAlias;
// Lookup propertyEditor from the registered ParameterEditors with the implmementation to avoid looking up for each parameter
IDataEditor? parameterEditor = parameterEditors.FirstOrDefault(f =>
string.Equals(f.Alias, parameterEditorAlias, StringComparison.OrdinalIgnoreCase));
IDataEditor? parameterEditor = parameterEditors.FirstOrDefault(f => string.Equals(f.Alias, parameterEditorAlias, StringComparison.OrdinalIgnoreCase));
if (parameterEditor is not null)
{
// Get the ParameterValueEditor for this PropertyEditor (where the GetReferences method is implemented) - cast as IDataValueReference to determine if 'it is' implemented for the editor
if (parameterEditor.GetValueEditor() is IDataValueReference parameterValueEditor)
foreach (UmbracoEntityReference entityReference in _dataValueReferenceFactories.GetReferences(parameterEditor, parameterValue))
{
foreach (UmbracoEntityReference entityReference in parameterValueEditor.GetReferences(
parameterValue))
{
foundUmbracoEntityReferences.Add(entityReference);
}
}
else
{
_logger.LogInformation(
"{0} doesn't have a ValueEditor that implements IDataValueReference",
parameterEditor.Alias);
yield return entityReference;
}
}
}
}
return foundUmbracoEntityReferences;
}
// Poco class to deserialise the Json for a Macro Control
@@ -11,36 +11,36 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Examine.Core" Version="3.0.1" />
<PackageReference Include="HtmlAgilityPack" Version="1.11.43" />
<PackageReference Include="IPNetwork2" Version="2.5.422" />
<PackageReference Include="MailKit" Version="3.2.0" />
<PackageReference Include="Markdown" Version="2.2.1" />
<PackageReference Include="Microsoft.CSharp" Version="4.7.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="6.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="6.0.0" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="6.0.0" />
<PackageReference Include="Microsoft.Extensions.Http" Version="6.0.0" />
<PackageReference Include="Microsoft.Extensions.Identity.Stores" Version="6.0.5" />
<PackageReference Include="MiniProfiler.Shared" Version="4.2.22" />
<PackageReference Include="ncrontab" Version="3.3.1" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
<PackageReference Include="NPoco.SqlServer" Version="5.3.2" />
<PackageReference Include="Serilog" Version="2.11.0" />
<PackageReference Include="Serilog.Enrichers.Process" Version="2.0.2" />
<PackageReference Include="Serilog.Enrichers.Thread" Version="3.1.0" />
<PackageReference Include="Serilog.Expressions" Version="3.4.0" />
<PackageReference Include="Serilog.Extensions.Hosting" Version="4.2.0" />
<PackageReference Include="Serilog.Formatting.Compact" Version="1.1.0" />
<PackageReference Include="Serilog.Formatting.Compact.Reader" Version="1.0.5" />
<PackageReference Include="Serilog.Settings.Configuration" Version="3.3.0" />
<PackageReference Include="Serilog.Sinks.Async" Version="1.5.0" />
<PackageReference Include="Serilog.Sinks.File" Version="5.0.0" />
<PackageReference Include="Serilog.Sinks.Map" Version="1.0.2" />
<PackageReference Include="SixLabors.ImageSharp" Version="2.1.3" />
<PackageReference Include="System.IO.FileSystem.AccessControl" Version="5.0.0" />
<PackageReference Include="System.Security.Cryptography.Pkcs" Version="6.0.1" />
<PackageReference Include="System.Threading.Tasks.Dataflow" Version="6.0.0" />
<PackageReference Include="Examine.Core"/>
<PackageReference Include="HtmlAgilityPack"/>
<PackageReference Include="IPNetwork2" />
<PackageReference Include="MailKit" />
<PackageReference Include="Markdown" />
<PackageReference Include="Microsoft.CSharp" />
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" />
<PackageReference Include="Microsoft.Extensions.Http" />
<PackageReference Include="Microsoft.Extensions.Identity.Stores" />
<PackageReference Include="MiniProfiler.Shared" />
<PackageReference Include="ncrontab" />
<PackageReference Include="Newtonsoft.Json" />
<PackageReference Include="NPoco.SqlServer" />
<PackageReference Include="Serilog" />
<PackageReference Include="Serilog.Enrichers.Process" />
<PackageReference Include="Serilog.Enrichers.Thread" />
<PackageReference Include="Serilog.Expressions" />
<PackageReference Include="Serilog.Extensions.Hosting" />
<PackageReference Include="Serilog.Formatting.Compact" />
<PackageReference Include="Serilog.Formatting.Compact.Reader" />
<PackageReference Include="Serilog.Settings.Configuration" />
<PackageReference Include="Serilog.Sinks.Async" />
<PackageReference Include="Serilog.Sinks.File" />
<PackageReference Include="Serilog.Sinks.Map" />
<PackageReference Include="SixLabors.ImageSharp" />
<PackageReference Include="System.IO.FileSystem.AccessControl" />
<PackageReference Include="System.Security.Cryptography.Pkcs" />
<PackageReference Include="System.Threading.Tasks.Dataflow" />
</ItemGroup>
<ItemGroup>
@@ -36,7 +36,7 @@ public class ContentCache : PublishedCacheBase, IPublishedContentCache, INavigab
IDomainCache domainCache,
IOptions<GlobalSettings> globalSettings,
IVariationContextAccessor variationContextAccessor)
: base(previewDefault)
: base(variationContextAccessor, previewDefault)
{
_snapshot = snapshot;
_snapshotCache = snapshotCache;
@@ -27,6 +27,8 @@ namespace Umbraco.Cms.Infrastructure.PublishedCache;
/// </remarks>
public class ContentStore
{
private static readonly TimeSpan _monitorTimeout = TimeSpan.FromSeconds(30);
// TODO: collection trigger (ok for now)
// see SnapDictionary notes
private const long CollectMinGenDelta = 8;
@@ -330,7 +332,12 @@ public class ContentStore
throw new InvalidOperationException("Recursive locks not allowed");
}
Monitor.Enter(_wlocko, ref lockInfo.Taken);
Monitor.TryEnter(_wlocko, _monitorTimeout, ref lockInfo.Taken);
if (Monitor.IsEntered(_wlocko) is false)
{
throw new TimeoutException("Could not enter monitor before timeout in content store");
}
lock (_rlocko)
{
@@ -736,16 +743,22 @@ public class ContentStore
{
EnsureLocked();
IPublishedContentType?[] contentTypes = _contentTypesById
IPublishedContentType[] contentTypes = _contentTypesById
.Where(kvp =>
kvp.Value.Value != null &&
kvp.Value.Value.PropertyTypes.Any(p => dataTypeIds.Contains(p.DataType.Id)))
.Select(kvp => kvp.Value.Value)
.Select(x => getContentType(x!.Id))
.Where(x => x != null) // poof, gone, very unlikely and probably an anomaly
.WhereNotNull() // poof, gone, very unlikely and probably an anomaly
.ToArray();
var contentTypeIdsA = contentTypes.Select(x => x!.Id).ToArray();
// all content types that are affected by this data type update must be updated
foreach (IPublishedContentType contentType in contentTypes)
{
SetContentTypeLocked(contentType);
}
var contentTypeIdsA = contentTypes.Select(x => x.Id).ToArray();
var contentTypeNodes = new Dictionary<int, List<int>>();
foreach (var id in contentTypeIdsA)
{
@@ -761,7 +774,7 @@ public class ContentStore
}
}
foreach (IPublishedContentType contentType in contentTypes.WhereNotNull())
foreach (IPublishedContentType contentType in contentTypes)
{
// again, weird situation
if (contentTypeNodes.ContainsKey(contentType.Id) == false)
@@ -17,7 +17,7 @@ public class MediaCache : PublishedCacheBase, IPublishedMediaCache, INavigableDa
#region Constructors
public MediaCache(bool previewDefault, ContentStore.Snapshot snapshot, IVariationContextAccessor variationContextAccessor)
: base(previewDefault)
: base(variationContextAccessor, previewDefault)
{
_snapshot = snapshot;
_variationContextAccessor = variationContextAccessor;
@@ -301,7 +301,7 @@ AND cmsContentNu.nodeId IS NULL
IContentCacheDataSerializer serializer =
_contentCacheDataSerializerFactory.Create(ContentCacheDataSerializerEntityType.Media);
IEnumerable<ContentSourceDto> dtos = GetContentNodeDtos(sql);
IEnumerable<ContentSourceDto> dtos = GetMediaNodeDtos(sql);
foreach (ContentSourceDto row in dtos)
{
@@ -319,7 +319,7 @@ AND cmsContentNu.nodeId IS NULL
IContentCacheDataSerializer serializer =
_contentCacheDataSerializerFactory.Create(ContentCacheDataSerializerEntityType.Media);
IEnumerable<ContentSourceDto> dtos = GetContentNodeDtos(sql);
IEnumerable<ContentSourceDto> dtos = GetMediaNodeDtos(sql);
foreach (ContentSourceDto row in dtos)
{
@@ -342,7 +342,7 @@ AND cmsContentNu.nodeId IS NULL
IContentCacheDataSerializer serializer =
_contentCacheDataSerializerFactory.Create(ContentCacheDataSerializerEntityType.Media);
IEnumerable<ContentSourceDto> dtos = GetContentNodeDtos(sql);
IEnumerable<ContentSourceDto> dtos = GetMediaNodeDtos(sql);
foreach (ContentSourceDto row in dtos)
{
@@ -990,6 +990,31 @@ WHERE cmsContentNu.nodeId IN (
return s;
}
private IEnumerable<ContentSourceDto> GetMediaNodeDtos(Sql<ISqlContext> sql)
{
// We need to page here. We don't want to iterate over every single row in one connection cuz this can cause an SQL Timeout.
// We also want to read with a db reader and not load everything into memory, QueryPaged lets us do that.
// QueryPaged is very slow on large sites however, so use fetch if UsePagedSqlQuery is disabled.
IEnumerable<ContentSourceDto> dtos;
if (_nucacheSettings.Value.UsePagedSqlQuery)
{
// Use a more efficient COUNT query
Sql<ISqlContext>? sqlCountQuery = SqlMediaSourcesCount()
.Append(SqlObjectTypeNotTrashed(SqlContext, Constants.ObjectTypes.Media));
Sql<ISqlContext>? sqlCount =
SqlContext.Sql("SELECT COUNT(*) FROM (").Append(sqlCountQuery).Append(") npoco_tbl");
dtos = Database.QueryPaged<ContentSourceDto>(_nucacheSettings.Value.SqlPageSize, sql, sqlCount);
}
else
{
dtos = Database.Fetch<ContentSourceDto>(sql);
}
return dtos;
}
private IEnumerable<ContentSourceDto> GetContentNodeDtos(Sql<ISqlContext> sql)
{
// We need to page here. We don't want to iterate over every single row in one connection cuz this can cause an SQL Timeout.
@@ -127,9 +127,25 @@ public class NuCacheContentService : RepositoryService, INuCacheContentService
{
using (ICoreScope scope = ScopeProvider.CreateCoreScope(repositoryCacheMode: RepositoryCacheMode.Scoped))
{
scope.ReadLock(Constants.Locks.ContentTree);
scope.ReadLock(Constants.Locks.MediaTree);
scope.ReadLock(Constants.Locks.MemberTree);
if (contentTypeIds is null && mediaTypeIds is null && memberTypeIds is null)
{
scope.ReadLock(Constants.Locks.ContentTree,Constants.Locks.MediaTree,Constants.Locks.MemberTree);
}
if (contentTypeIds is not null && contentTypeIds.Any())
{
scope.ReadLock(Constants.Locks.ContentTree);
}
if (mediaTypeIds is not null && mediaTypeIds.Any())
{
scope.ReadLock(Constants.Locks.MediaTree);
}
if (memberTypeIds is not null && memberTypeIds.Any())
{
scope.ReadLock(Constants.Locks.MemberTree);
}
_repository.Rebuild(contentTypeIds, mediaTypeIds, memberTypeIds);
@@ -56,7 +56,7 @@ internal class PublishedContent : PublishedContentBase
// add one property per property type - this is required, for the indexing to work
// if contentData supplies pdatas, use them, else use null
contentData.Properties.TryGetValue(propertyType.Alias, out PropertyData[]? pdatas); // else will be null
properties[i++] = new Property(propertyType, this, pdatas, _publishedSnapshotAccessor);
properties[i++] = new Property(propertyType, this, pdatas, _publishedSnapshotAccessor, propertyType.CacheLevel);
}
PropertiesArray = properties;
@@ -210,7 +210,16 @@ internal class PublishedSnapshotService : IPublishedSnapshotService
// they require.
using (_contentStore.GetScopedWriteLock(_scopeProvider))
{
NotifyLocked(new[] { new ContentCacheRefresher.JsonPayload(0, null, TreeChangeTypes.RefreshAll) }, out _, out _);
NotifyLocked(
new[]
{
new ContentCacheRefresher.JsonPayload()
{
ChangeTypes = TreeChangeTypes.RefreshAll
}
},
out _,
out _);
}
using (_mediaStore.GetScopedWriteLock(_scopeProvider))
@@ -852,6 +861,12 @@ internal class PublishedSnapshotService : IPublishedSnapshotService
// contentStore is write-locked during changes - see note above, calls to this method are wrapped in contentStore.GetScopedWriteLock
foreach (ContentCacheRefresher.JsonPayload payload in payloads)
{
if (payload.Blueprint)
{
// Skip blueprints
continue;
}
_logger.LogDebug("Notified {ChangeTypes} for content {ContentId}", payload.ChangeTypes, payload.Id);
if (payload.ChangeTypes.HasType(TreeChangeTypes.RefreshAll))
@@ -9,6 +9,8 @@ public class SnapDictionary<TKey, TValue>
where TValue : class
where TKey : notnull
{
private static readonly TimeSpan _monitorTimeout = TimeSpan.FromSeconds(30);
// minGenDelta to be adjusted
// we may want to throttle collects even if delta is reached
// we may want to force collect if delta is not reached but very old
@@ -198,7 +200,12 @@ public class SnapDictionary<TKey, TValue>
throw new InvalidOperationException("Recursive locks not allowed");
}
Monitor.Enter(_wlocko, ref lockInfo.Taken);
Monitor.TryEnter(_wlocko, _monitorTimeout, ref lockInfo.Taken);
if (Monitor.IsEntered(_wlocko) is false)
{
throw new TimeoutException("Could not enter the monitor before timeout in SnapDictionary");
}
lock (_rlocko)
{
@@ -7,10 +7,10 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Umbraco.CSharpTest.Net.Collections" Version="14.906.1403.1085" />
<PackageReference Include="MessagePack" Version="2.3.85" />
<PackageReference Include="K4os.Compression.LZ4" Version="1.2.16" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
<PackageReference Include="Umbraco.CSharpTest.Net.Collections" />
<PackageReference Include="MessagePack" />
<PackageReference Include="K4os.Compression.LZ4" />
<PackageReference Include="Newtonsoft.Json"/>
</ItemGroup>
<ItemGroup>
@@ -19,6 +19,8 @@ public class
{
private readonly ContentPermissions _contentPermissions;
protected override UmbracoObjectTypes KeyParsingFilterType => UmbracoObjectTypes.Document;
/// <summary>
/// Initializes a new instance of the <see cref="ContentPermissionsQueryStringHandler" /> class.
/// </summary>
@@ -47,7 +49,11 @@ public class
return Task.FromResult(true);
}
var argument = routeVal.ToString();
// Handle case where the incoming querystring could contain more than one value (e.g. ?id=1000&id=1001).
// It's the first one that'll be processed by the protected method so we should verify that.
var argument = routeVal.Count == 1
? routeVal.ToString()
: routeVal.FirstOrDefault()?.ToString() ?? string.Empty;
if (!TryParseNodeId(argument, out nodeId))
{
@@ -18,6 +18,8 @@ public class MediaPermissionsQueryStringHandler : PermissionsQueryStringHandler<
{
private readonly MediaPermissions _mediaPermissions;
protected override UmbracoObjectTypes KeyParsingFilterType => UmbracoObjectTypes.Media;
/// <summary>
/// Initializes a new instance of the <see cref="MediaPermissionsQueryStringHandler" /> class.
/// </summary>
@@ -44,7 +46,11 @@ public class MediaPermissionsQueryStringHandler : PermissionsQueryStringHandler<
return Task.FromResult(true);
}
var argument = routeVal.ToString();
// Handle case where the incoming querystring could contain more than one value (e.g. ?id=1000&id=1001).
// It's the first one that'll be processed by the protected method so we should verify that.
var argument = routeVal.Count == 1
? routeVal.ToString()
: routeVal.FirstOrDefault()?.ToString() ?? string.Empty;
if (!TryParseNodeId(argument, out var nodeId))
{
@@ -49,12 +49,18 @@ public abstract class PermissionsQueryStringHandler<T> : MustSatisfyRequirementA
/// </summary>
protected IEntityService EntityService { get; set; }
/// <summary>
/// Defaults to Unknown so all types are allowed, since Keys are unique across all node types this works,
/// but it if you are certain you are looking for a specific type this should be overwritten for DB query performance.
/// </summary>
protected virtual UmbracoObjectTypes KeyParsingFilterType => UmbracoObjectTypes.Unknown;
/// <summary>
/// Attempts to parse a node ID from a string representation found in a querystring value.
/// </summary>
/// <param name="argument">Querystring value.</param>
/// <param name="nodeId">Output parsed Id.</param>
/// <returns>True of node ID could be parased, false it not.</returns>
/// <returns>True of node ID could be parsed, false it not.</returns>
protected bool TryParseNodeId(string argument, out int nodeId)
{
// If the argument is an int, it will parse and can be assigned to nodeId.
@@ -75,7 +81,7 @@ public abstract class PermissionsQueryStringHandler<T> : MustSatisfyRequirementA
if (Guid.TryParse(argument, out Guid key))
{
nodeId = EntityService.GetId(key, UmbracoObjectTypes.Document).Result;
nodeId = EntityService.GetId(key, KeyParsingFilterType).Result;
return true;
}
@@ -1,9 +1,12 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Web.Common.Authorization;
namespace Umbraco.Cms.Web.BackOffice.Controllers;
[Authorize(Policy = AuthorizationPolicies.SectionAccessSettings)]
public class AnalyticsController : UmbracoAuthorizedJsonController
{
private readonly IMetricsConsentService _metricsConsentService;
@@ -74,6 +74,9 @@ public class AuthenticationController : UmbracoApiControllerBase
private readonly IUserService _userService;
private readonly WebRoutingSettings _webRoutingSettings;
private const int FailedLoginDurationRandomOffsetInMilliseconds = 100;
private static long? _loginDurationAverage;
// TODO: We need to review all _userManager.Raise calls since many/most should be on the usermanager or signinmanager, very few should be here
[ActivatorUtilitiesConstructor]
public AuthenticationController(
@@ -129,12 +132,17 @@ public class AuthenticationController : UmbracoApiControllerBase
AuthorizationPolicies.BackOfficeAccess)] // Needed to enforce the principle set on the request, if one exists.
public IDictionary<string, object> GetPasswordConfig(int userId)
{
if (HttpContext.HasActivePasswordResetFlowSession(userId))
{
return _passwordConfiguration.GetConfiguration();
}
Attempt<int> currentUserId =
_backofficeSecurityAccessor.BackOfficeSecurity?.GetUserId() ?? Attempt<int>.Fail();
return _passwordConfiguration.GetConfiguration(
currentUserId.Success
? currentUserId.Result != userId
: true);
return currentUserId.Success
? _passwordConfiguration.GetConfiguration(currentUserId.Result != userId)
: new Dictionary<string, object>();
}
/// <summary>
@@ -342,47 +350,85 @@ public class AuthenticationController : UmbracoApiControllerBase
[Authorize(Policy = AuthorizationPolicies.DenyLocalLoginIfConfigured)]
public async Task<ActionResult<UserDetail?>> PostLogin(LoginModel loginModel)
{
HttpContext.EndPasswordResetFlowSession();
// Start a timed scope to ensure failed responses return is a consistent time
await using var timedScope = new TimedScope(GetLoginDuration(), CancellationToken.None);
// Sign the user in with username/password, this also gives a chance for developers to
// custom verify the credentials and auto-link user accounts with a custom IBackOfficePasswordChecker
SignInResult result = await _signInManager.PasswordSignInAsync(
loginModel.Username, loginModel.Password, true, true);
if (result.Succeeded)
if (result.Succeeded is false)
{
// return the user detail
return GetUserDetail(_userService.GetByUsername(loginModel.Username));
}
BackOfficeIdentityUser? user = await _userManager.FindByNameAsync(loginModel.Username.Trim());
if (result.RequiresTwoFactor)
{
var twofactorView = _backOfficeTwoFactorOptions.GetTwoFactorView(loginModel.Username);
if (twofactorView.IsNullOrWhiteSpace())
if (user is not null &&
await _userManager.CheckPasswordAsync(user, loginModel.Password))
{
return new ValidationErrorResult(
$"The registered {typeof(IBackOfficeTwoFactorOptions)} of type {_backOfficeTwoFactorOptions.GetType()} did not return a view for two factor auth ");
// The credentials were correct, so cancel timed scope and provide a more detailed failure response
timedScope.Cancel();
if (result.RequiresTwoFactor)
{
var twofactorView = _backOfficeTwoFactorOptions.GetTwoFactorView(loginModel.Username);
if (twofactorView.IsNullOrWhiteSpace())
{
return new ValidationErrorResult(
$"The registered {typeof(IBackOfficeTwoFactorOptions)} of type {_backOfficeTwoFactorOptions.GetType()} did not return a view for two factor auth ");
}
IUser? attemptedUser = _userService.GetByUsername(loginModel.Username);
// create a with information to display a custom two factor send code view
var verifyResponse =
new ObjectResult(new { twoFactorView = twofactorView, userId = attemptedUser?.Id })
{
StatusCode = StatusCodes.Status402PaymentRequired
};
return verifyResponse;
}
// TODO: We can check for these and respond differently if we think it's important
// result.IsLockedOut
// result.IsNotAllowed
}
IUser? attemptedUser = _userService.GetByUsername(loginModel.Username);
// create a with information to display a custom two factor send code view
var verifyResponse =
new ObjectResult(new { twoFactorView = twofactorView, userId = attemptedUser?.Id })
{
StatusCode = StatusCodes.Status402PaymentRequired
};
return verifyResponse;
// Return BadRequest (400), we don't want to return a 401 because that get's intercepted
// by our angular helper because it thinks that we need to re-perform the request once we are
// authorized and we don't want to return a 403 because angular will show a warning message indicating
// that the user doesn't have access to perform this function, we just want to return a normal invalid message.
return BadRequest();
}
// TODO: We can check for these and respond differently if we think it's important
// result.IsLockedOut
// result.IsNotAllowed
// Set initial or update average (successful) login duration
_loginDurationAverage = _loginDurationAverage is long average
? (average + (long)timedScope.Elapsed.TotalMilliseconds) / 2
: (long)timedScope.Elapsed.TotalMilliseconds;
// return BadRequest (400), we don't want to return a 401 because that get's intercepted
// by our angular helper because it thinks that we need to re-perform the request once we are
// authorized and we don't want to return a 403 because angular will show a warning message indicating
// that the user doesn't have access to perform this function, we just want to return a normal invalid message.
return BadRequest();
// Cancel the timed scope (we don't want to unnecessarily wait on a successful response)
timedScope.Cancel();
// Return the user detail
return GetUserDetail(_userService.GetByUsername(loginModel.Username));
}
private long GetLoginDuration()
{
var loginDuration = Math.Max(_loginDurationAverage ?? _securitySettings.UserDefaultFailedLoginDurationInMilliseconds, _securitySettings.UserMinimumFailedLoginDurationInMilliseconds);
var random = new Random();
var randomDelay = random.Next(-FailedLoginDurationRandomOffsetInMilliseconds, FailedLoginDurationRandomOffsetInMilliseconds);
loginDuration += randomDelay;
// Just be sure we don't get a negative number - possible if someone has configured a very low UserMinimumFailedLoginDurationInMilliseconds value.
if (loginDuration < 0)
{
loginDuration = 0;
}
return loginDuration;
}
/// <summary>
@@ -401,7 +447,12 @@ public class AuthenticationController : UmbracoApiControllerBase
return BadRequest();
}
HttpContext.EndPasswordResetFlowSession();
BackOfficeIdentityUser? identityUser = await _userManager.FindByEmailAsync(model.Email);
await Task.Delay(RandomNumberGenerator.GetInt32(400, 2500)); // To randomize response time preventing user enumeration
if (identityUser != null)
{
IUser? user = _userService.GetByEmail(model.Email);
@@ -422,14 +473,20 @@ public class AuthenticationController : UmbracoApiControllerBase
var mailMessage = new EmailMessage(from, user.Email, subject, message, true);
await _emailSender.SendAsync(mailMessage, Constants.Web.EmailTypes.PasswordReset, true);
try
{
await _emailSender.SendAsync(mailMessage, Constants.Web.EmailTypes.PasswordReset, true);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error sending email, please check your SMTP configuration: {ErrorMessage}", ex.Message);
return Ok();
}
_userManager.NotifyForgotPasswordRequested(User, user.Id.ToString());
}
}
await Task.Delay(RandomNumberGenerator.GetInt32(400, 2500));
return Ok();
}
@@ -545,6 +602,8 @@ public class AuthenticationController : UmbracoApiControllerBase
[AllowAnonymous]
public async Task<IActionResult> PostSetPassword(SetPasswordModel model)
{
HttpContext.EndPasswordResetFlowSession();
BackOfficeIdentityUser? identityUser =
await _userManager.FindByIdAsync(model.UserId.ToString(CultureInfo.InvariantCulture));
@@ -370,6 +370,8 @@ public class BackOfficeController : UmbracoController
var result = await _userManager.VerifyUserTokenAsync(user, "Default", "ResetPassword", resetCode);
if (result)
{
HttpContext.StartPasswordResetFlowSession(userId);
//Add a flag and redirect for it to be displayed
TempData[ViewDataExtensions.TokenPasswordResetCode] =
_jsonSerializer.Serialize(
@@ -16,6 +16,7 @@ using Umbraco.Cms.Core.Models.ContentEditing;
using Umbraco.Cms.Core.Models.Editors;
using Umbraco.Cms.Core.Models.Membership;
using Umbraco.Cms.Core.Models.Validation;
using Umbraco.Cms.Core.Notifications;
using Umbraco.Cms.Core.Persistence.Querying;
using Umbraco.Cms.Core.PropertyEditors;
using Umbraco.Cms.Core.Routing;
@@ -195,6 +196,7 @@ public class ContentController : ContentControllerBase
/// Permission check is done for letter 'R' which is for <see cref="ActionRights" /> which the user must have access to
/// update
/// </remarks>
[HttpPost]
public async Task<ActionResult<IEnumerable<AssignedUserGroupPermissions?>?>> PostSaveUserGroupPermissions(
UserGroupPermissionsSave saveModel)
{
@@ -623,17 +625,33 @@ public class ContentController : ContentControllerBase
[OutgoingEditorModelEvent]
public ActionResult<ContentItemDisplay?> GetEmptyBlueprint(int blueprintId, int parentId)
{
IContent? blueprint = _contentService.GetBlueprintById(blueprintId);
if (blueprint == null)
IContent? scaffold;
using (ICoreScope scope = _scopeProvider.CreateCoreScope())
{
return NotFound();
IContent? blueprint = _contentService.GetBlueprintById(blueprintId);
if (blueprint is null)
{
return NotFound();
}
scaffold = (IContent)blueprint.DeepClone();
scaffold.Id = 0;
scaffold.Name = string.Empty;
scaffold.ParentId = parentId;
var scaffoldedNotification = new ContentScaffoldedNotification(blueprint, scaffold, parentId, new EventMessages());
if (scope.Notifications.PublishCancelable(scaffoldedNotification))
{
scope.Complete();
return Problem("Scaffolding was cancelled");
}
scope.Complete();
}
blueprint.Id = 0;
blueprint.Name = string.Empty;
blueprint.ParentId = parentId;
ContentItemDisplay? mapped = _umbracoMapper.Map<ContentItemDisplay>(blueprint);
ContentItemDisplay? mapped = _umbracoMapper.Map<ContentItemDisplay>(scaffold);
if (mapped is not null)
{
@@ -759,12 +777,14 @@ public class ContentController : ContentControllerBase
return pagedResult;
}
/// <summary>
/// Creates a blueprint from a content item
/// </summary>
/// <param name="contentId">The content id to copy</param>
/// <param name="name">The name of the blueprint</param>
/// <returns></returns>
[Authorize(Policy = AuthorizationPolicies.ContentPermissionCreateBlueprintFromId)]
[HttpPost]
public ActionResult<SimpleNotificationModel> CreateBlueprintFromContent(
[FromQuery] int contentId,
@@ -820,8 +840,10 @@ public class ContentController : ContentControllerBase
/// <summary>
/// Saves content
/// </summary>
[Authorize(Policy = AuthorizationPolicies.TreeAccessDocumentTypes)]
[FileUploadCleanupFilter]
[ContentSaveValidation]
[ContentSaveValidation(skipUserAccessValidation:true)] // skip user access validation because we "only" require Settings access to create new blueprints from scratch
[HttpPost]
public async Task<ActionResult<ContentItemDisplay<ContentVariantDisplay>?>?> PostSaveBlueprint(
[ModelBinder(typeof(BlueprintItemBinder))] ContentItemSave contentItem)
{
@@ -859,6 +881,7 @@ public class ContentController : ContentControllerBase
[FileUploadCleanupFilter]
[ContentSaveValidation]
[OutgoingEditorModelEvent]
[HttpPost]
public async Task<ActionResult<ContentItemDisplay<ContentVariantScheduleDisplay>?>> PostSave(
[ModelBinder(typeof(ContentItemBinder))] ContentItemSave contentItem)
{
@@ -1057,7 +1080,7 @@ public class ContentController : ContentControllerBase
AddDomainWarnings(publishStatus.Content, successfulCultures, globalNotifications);
AddPublishStatusNotifications(new[] { publishStatus }, globalNotifications, notifications, successfulCultures);
}
break;
break;
case ContentSaveAction.PublishWithDescendants:
case ContentSaveAction.PublishWithDescendantsNew:
{
@@ -1074,7 +1097,7 @@ public class ContentController : ContentControllerBase
AddDomainWarnings(publishStatus, successfulCultures, globalNotifications);
AddPublishStatusNotifications(publishStatus, globalNotifications, notifications, successfulCultures);
}
break;
break;
case ContentSaveAction.PublishWithDescendantsForce:
case ContentSaveAction.PublishWithDescendantsForceNew:
{
@@ -1090,7 +1113,7 @@ public class ContentController : ContentControllerBase
var publishStatus = PublishBranchInternal(contentItem, true, cultureForInvariantErrors, out wasCancelled, out var successfulCultures).ToList();
AddPublishStatusNotifications(publishStatus, globalNotifications, notifications, successfulCultures);
}
break;
break;
default:
throw new ArgumentOutOfRangeException();
}
@@ -1940,6 +1963,7 @@ public class ContentController : ContentControllerBase
/// does not have Publish access to this node.
/// </remarks>
[Authorize(Policy = AuthorizationPolicies.ContentPermissionPublishById)]
[HttpPost]
public IActionResult PostPublishById(int id)
{
IContent? foundContent = GetObjectFromRequest(() => _contentService.GetById(id));
@@ -1971,6 +1995,7 @@ public class ContentController : ContentControllerBase
/// does not have Publish access to this node.
/// </remarks>
[Authorize(Policy = AuthorizationPolicies.ContentPermissionPublishById)]
[HttpPost]
public IActionResult PostPublishByIdAndCulture(PublishContent model)
{
var languageCount = _allLangs.Value.Count();
@@ -2011,6 +2036,7 @@ public class ContentController : ContentControllerBase
return Ok();
}
[Authorize(Policy = AuthorizationPolicies.TreeAccessDocumentTypes)]
[HttpDelete]
[HttpPost]
public IActionResult DeleteBlueprint(int id)
@@ -2093,6 +2119,7 @@ public class ContentController : ContentControllerBase
/// </summary>
/// <param name="sorted"></param>
/// <returns></returns>
[HttpPost]
public async Task<IActionResult> PostSort(ContentSortOrder sorted)
{
if (sorted == null)
@@ -2144,6 +2171,7 @@ public class ContentController : ContentControllerBase
/// </summary>
/// <param name="move"></param>
/// <returns></returns>
[HttpPost]
public async Task<IActionResult?> PostMove(MoveOrCopy move)
{
// Authorize...
@@ -2178,6 +2206,7 @@ public class ContentController : ContentControllerBase
/// </summary>
/// <param name="copy"></param>
/// <returns></returns>
[HttpPost]
public async Task<ActionResult<IContent>?> PostCopy(MoveOrCopy copy)
{
// Authorize...
@@ -2217,6 +2246,7 @@ public class ContentController : ContentControllerBase
/// <param name="model">The content and variants to unpublish</param>
/// <returns></returns>
[OutgoingEditorModelEvent]
[HttpPost]
public async Task<ActionResult<ContentItemDisplayWithSchedule?>> PostUnpublish(UnpublishContent model)
{
IContent? foundContent = _contentService.GetById(model.Id);
@@ -2336,11 +2366,11 @@ public class ContentController : ContentControllerBase
return NotFound("There is no content node with id {model.NodeId}.");
}
EntityPermission? permission =
_userService.GetPermissions(_backofficeSecurityAccessor.BackOfficeSecurity?.CurrentUser, node.Path);
// Validate permissions on node
var permissions = _userService.GetAllPermissions(_backofficeSecurityAccessor.BackOfficeSecurity?.CurrentUser, node.Path);
if (permission?.AssignedPermissions.Contains(ActionAssignDomain.ActionLetter.ToString(), StringComparer.Ordinal) == false)
if (permissions.Any(x =>
x.AssignedPermissions.Contains(ActionAssignDomain.ActionLetter.ToString(), StringComparer.Ordinal) && x.EntityId == node.Id) == false)
{
HttpContext.SetReasonPhrase("Permission Denied.");
return BadRequest("You do not have permission to assign domains on that node.");
@@ -2745,7 +2775,7 @@ public class ContentController : ContentControllerBase
}
}
}
break;
break;
case PublishResultType.SuccessPublish:
{
// TODO: Here we should have messaging for when there are release dates specified like https://github.com/umbraco/Umbraco-CMS/pull/3507
@@ -2773,7 +2803,7 @@ public class ContentController : ContentControllerBase
}
}
}
break;
break;
case PublishResultType.FailedPublishPathNotPublished:
{
//TODO: This doesn't take into account variations with the successfulCultures param
@@ -2782,14 +2812,14 @@ public class ContentController : ContentControllerBase
_localizedTextService.Localize(null, "publish"),
_localizedTextService.Localize("publish", "contentPublishedFailedByParent", new[] { names }).Trim());
}
break;
break;
case PublishResultType.FailedPublishCancelledByEvent:
{
//TODO: This doesn't take into account variations with the successfulCultures param
var names = string.Join(", ", status.Select(x => $"'{x.Content?.Name}'"));
AddCancelMessage(display, "publish", "contentPublishedFailedByEvent", new[] { names });
}
break;
break;
case PublishResultType.FailedPublishAwaitingRelease:
{
//TODO: This doesn't take into account variations with the successfulCultures param
@@ -2798,7 +2828,7 @@ public class ContentController : ContentControllerBase
_localizedTextService.Localize(null, "publish"),
_localizedTextService.Localize("publish", "contentPublishedFailedAwaitingRelease", new[] { names }).Trim());
}
break;
break;
case PublishResultType.FailedPublishHasExpired:
{
//TODO: This doesn't take into account variations with the successfulCultures param
@@ -2807,7 +2837,7 @@ public class ContentController : ContentControllerBase
_localizedTextService.Localize(null, "publish"),
_localizedTextService.Localize("publish", "contentPublishedFailedExpired", new[] { names }).Trim());
}
break;
break;
case PublishResultType.FailedPublishIsTrashed:
{
//TODO: This doesn't take into account variations with the successfulCultures param
@@ -2816,7 +2846,7 @@ public class ContentController : ContentControllerBase
_localizedTextService.Localize(null, "publish"),
_localizedTextService.Localize("publish", "contentPublishedFailedIsTrashed", new[] { names }).Trim());
}
break;
break;
case PublishResultType.FailedPublishContentInvalid:
{
if (successfulCultures == null)
@@ -2840,7 +2870,7 @@ public class ContentController : ContentControllerBase
}
}
}
break;
break;
case PublishResultType.FailedPublishMandatoryCultureMissing:
display.AddWarningNotification(
_localizedTextService.Localize(null, "publish"),
@@ -2939,6 +2969,7 @@ public class ContentController : ContentControllerBase
return notifications;
}
[HttpPost]
public IActionResult PostNotificationOptions(
int contentId,
[FromQuery(Name = "notifyOptions[]")] string[] notifyOptions)
@@ -79,7 +79,6 @@ public abstract class ContentControllerBase : BackOfficeNotificationsController
ModelState.AddModelError("id", $"content with id: {id} was not found");
NotFoundObjectResult errorResponse = NotFound(ModelState);
return errorResponse;
}
@@ -7,6 +7,7 @@ using Umbraco.Cms.Core.Configuration.Models;
using Umbraco.Cms.Core.IO;
using Umbraco.Cms.Core.Media;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Routing;
using Umbraco.Cms.Web.Common.Attributes;
using Umbraco.Cms.Web.Common.DependencyInjection;
using Umbraco.Extensions;
@@ -123,7 +124,7 @@ public class ImagesController : UmbracoAuthorizedApiController
private bool IsAllowed(string encodedImagePath)
{
if(Uri.IsWellFormedUriString(encodedImagePath, UriKind.Relative))
if(WebPath.IsWellFormedWebPath(encodedImagePath, UriKind.Relative))
{
return true;
}

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