Compare commits

...
251 Commits
Author SHA1 Message Date
1f5c21c631 Fix pagination in Content Delivery API Index Helper (#19606)
* Refactor descendant enumeration in DeliveryApiContentIndexHelper

Improved loop condition to allow for processing of more than 10.000 descendants for indexing.

* Add failing test for original issue.

* Renamed variable for clarity.

---------

Co-authored-by: Brynjar Þorsteinsson <brynjar@vettvangur.is>
Co-authored-by: Andy Butland <abutland73@gmail.com>
2025-07-01 08:11:00 +02:00
4ad18dc963 Preserve word boundaries when indexing RTE content with <br> tags (#19540)
* Preserve word boundaries when indexing RTE content with <br> tags

Replace <br> tags with spaces before HTML stripping to prevent word
concatenation in Examine index. Fixes issue where "John Smith<br>Company ABC"
was indexed as "John SmithCompany ABC" instead of "John Smith Company ABC".

- Add regex to replace <br> variants with spaces in RichTextPropertyIndexValueFactory
- Handles <br>, <br/> with spaces and attributes
- Maintains existing StripHtml() behavior for all other HTML tags

* Added unit test with test cases for expected index values

* Regex tweak to avoid matches on <break> <branything> etc

* Tweaked tests as per PR feedback

* Update tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/RichTextPropertyIndexValueFactoryTests.cs

Updated tests

Co-authored-by: Kenn Jacobsen <post@kennjacobsen.dk>

* Tweaked test expectations

---------

Co-authored-by: Emma Garland <emma.garland@rocksolidknowledge.com>
Co-authored-by: Emma L Garland <emmagarland77@gmail.com>
Co-authored-by: Kenn Jacobsen <post@kennjacobsen.dk>
2025-06-23 09:34:51 +00:00
Andy Butland 881535af7d Merge branch 'v13/dev' into v13/main 2025-06-13 07:20:11 +02:00
Laura NetoandGitHub 1e66fb6ab3 Add a warning to the CreateContentFromBlueprint method xml docs (#19542)
Add a remark to `ContentService.CreateContentFromBlueprint`
2025-06-13 07:15:19 +02:00
Andy Butland e1b9e5efad Merge branch 'release/13.9.1' into v13/dev
# Conflicts:
#	version.json
2025-06-10 09:56:08 +02:00
Andy ButlandandGitHub 28570b43e8 Fix issues with removal of user logins on change to external login provider configuration (13) (#19511)
* Ensure to delete related tokens when removing logins for removed external login providers.
Ensure to avoid removing logins for members.

* Applied suggestions from code review.

* Removed unnecessary <= check.
2025-06-10 08:19:52 +02:00
Andy Butland a01382d756 Merge branch 'release/13.9' into v13/dev 2025-06-05 10:00:12 +02:00
Andy Butland 1d6e7f15b9 Bumped version to 13.9.1. 2025-06-05 07:47:57 +02:00
Andy Butland 97cc3ca581 Bumped version to 13.9.0. 2025-06-05 06:24:08 +02:00
Sven GeusensandGitHub 0bcae3e87b Fix null member edgecase (#19463) 2025-06-03 03:32:03 +00:00
ebd228c3d7 Ensure tag operations are case insensitive on insert across database types (#19439)
* Ensure tag operations are case insensitve on insert across database types.

* Ensure tags provided in a single property are case insensitively distinct when saving the tags and relationships.

* Update src/Umbraco.Infrastructure/Persistence/Repositories/Implement/TagRepository.cs

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

* Handle case sensitivity on insert with tag groups too.

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-05-30 09:05:22 +02:00
Andy ButlandandGitHub 4b83a74bdb Resolved exception thrown from NuCache locking mechanism on near-simultaneous content publish requests (#19434)
* Removed the check before the wait that threw the recursive lock exception.
Added additional check to ensure we don't release a lock that's already released.

* Removed unnecessary check on releasing the lock.
2025-05-28 14:33:47 +02:00
Rowan BottemaandGitHub b348b84b63 Lock appropriate tree for media operations (#19422)
The MediaService currently locks the ContentTree for GetPagedOfType(s) operations, but it's querying the MediaTree. This ensures we lock the correct tree.
2025-05-27 06:51:30 +02:00
Andy Butland 7f4a8d5974 Merge branch 'v13/dev' into v13/main 2025-05-27 06:49:05 +02:00
Andy ButlandandGitHub 4d8ca457ec Removes unnecessary newlines from rich text as JSON delivery API output (#19391)
* Removes unnecessary newlines from rich text as JSON delivery API output.

* Fix case from PR feedback.
2025-05-23 12:19:43 +02:00
Andy ButlandandGitHub d677e948f1 Fix issue with requests to delivery API by path where URL segment contains special characters (#19390)
Fix issue with requests to delivery API by path where URL segment contains special characters.
2025-05-23 09:41:15 +02:00
Andy Butland d4e6af50bd Merge branch 'release/13.9' into v13/dev 2025-05-22 10:15:39 +02:00
Andy Butland c609cafa9b Fixed check for navigation to list after delete of member (#19364)
Fixed check for navigation to list after delete of member.
2025-05-22 10:14:52 +02:00
Andy ButlandandGitHub 5556b0fe0c Fixed check for navigation to list after delete of member (#19364)
Fixed check for navigation to list after delete of member.
2025-05-22 10:13:31 +02:00
Andy Butland eee04f4703 fix: check for NullRepresentationInCache in AppCacheExtensions (#19350)
* fix: add appcache null check

* Moved constant into standard location.
Removed now unnecessary comment.

---------

Co-authored-by: Andy Butland <abutland73@gmail.com>
# Conflicts:
#	src/Umbraco.Core/Cache/AppCacheExtensions.cs
#	src/Umbraco.Core/Constants-Cache.cs
2025-05-22 08:58:37 +02:00
127c6c4b53 Optimize the member save as part of the member login process, by-passing locking and audit steps and handling only the expected update properties (#19308)
* Optimize the member save as part of the member login process, by-passing locking and audit steps and handling only the expected update properties.

* Added unit test to verify new behaviour.

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

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

* Updates from code review.

* Improved comments.

* Add state information to notification indicating whether a member is saved via only the update of login properties.

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Migaroez <geusens@gmail.com>
2025-05-20 15:27:42 +01:00
7d6a1e54e6 Optimize the member save as part of the member login process, by-passing locking and audit steps and handling only the expected update properties (#19308)
* Optimize the member save as part of the member login process, by-passing locking and audit steps and handling only the expected update properties.

* Added unit test to verify new behaviour.

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

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

* Updates from code review.

* Improved comments.

* Add state information to notification indicating whether a member is saved via only the update of login properties.

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Migaroez <geusens@gmail.com>
2025-05-20 13:21:23 +00:00
David ChallenerandGitHub 8433b2b637 User invite email fails if visited more than once without completing (#17901)
Don't change securitystamp until user created
2025-05-14 10:03:53 +01:00
87b2153794 Adds support for retrieval of data type references when data type is routed using a GUID (#19184)
* Adds support for retrieval of data type references when data type is routed using a GUID.

* Fixed typos in comments.

* Use IIdKeyMap to resolve ID instead of fetching datatype

* Use IDKeyMap instead

---------

Co-authored-by: mole <nikolajlauridsen@protonmail.ch>
2025-05-14 10:54:00 +02:00
Andy ButlandandGitHub 0fc5b2a95c Avoids unnecessary additional domain save notification publishing when sorting an already sorted collection of domains (#19106)
Avoids unnecessary additional domain save notification publishings when sorting an already sorted collection of domains.
2025-05-14 10:25:49 +02:00
Andreas ZerbstandGitHub 6c2f93e85d V13 QA Updated linux test images to match actual (#19309)
Updated linux images
2025-05-14 07:19:32 +00:00
4f1604fea1 V13/bugfix/partial cache (#19314)
* Make sure that each optional section of the cachekey always starts and ends with a -

* Move secondary logic of clearing the membercaches into its own replacable class

* Regsiter the new implementation

* Add a mock to the integration tests as appCaches are disabled

* Added header comments to components.

* Refactored cache key into a method and exposed for testing.
Added unit tests to verify behaviour.

* Verified also that regex matches only the supplied member and asserted on the key itself.

---------

Co-authored-by: Andy Butland <abutland73@gmail.com>
2025-05-14 08:46:07 +02:00
059766291b Fix for block grid styling regression issue #15973 (#19014)
Co-authored-by: Andy Butland <abutland73@gmail.com>
2025-05-13 06:36:20 +02:00
e471c1fc8b [V13] User notifications not sent correctly when having more than 400 users (#18370)
Co-authored-by: Vedran Antović <vedran.antovic@isatis.com>
Co-authored-by: Andy Butland <abutland73@gmail.com>
2025-05-13 06:35:42 +02:00
0fb91ef859 add single blocklist delete (#18073)
Co-authored-by: Andy Butland <abutland73@gmail.com>
2025-05-12 10:36:39 +00:00
Andy Butland c0a0a34cbb Backport variable name updates and fix found porting the invalidation of sessions after removal of external login provider to 16. 2025-05-09 14:57:08 +02:00
Andy ButlandandGitHub 3d44a6fdf9 Invalidate external login session on removal of provider (#19273) 2025-05-09 10:22:13 +00:00
0eee307622 Added custom RichTextRegexValidator to validate markup instead of JSON (#19045)
Co-authored-by: Migaroez <geusens@gmail.com>
2025-05-08 14:51:06 +02:00
6a4aa04b47 Handle existing RichTextEditorValue when parsing from markup or JSON structure (#19266)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-05-08 10:29:40 +02:00
643f073945 TimedScope improvements and login duration clean-up (#19243)
Co-authored-by: Andy Butland <abutland73@gmail.com>
2025-05-07 18:30:49 +02:00
Andy Butland 5ba40c65fc Merge branch 'release/13.8.1' into v13/dev
# Conflicts:
#	version.json
2025-05-06 07:13:40 +02:00
Andy Butland dcbbed4160 Fixed failures in unit tests. 2025-05-06 05:35:21 +02:00
Andy Butland e94e165593 Fixed pipeline definition. 2025-05-06 05:17:05 +02:00
Andy ButlandandGitHub 34709be6cc Merge commit from fork
* Backport user enumeration fix.

* Bump gitversion.

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

* Adds randomness to failed login duration.
2025-05-06 05:11:04 +02:00
83107bb31a Fix: #18421 - Added Max Length validation to PropertyTypeBasic Alias (#18427)
Co-authored-by: Daniel Nelson <daniel.nelson@cantarus.com>
2025-05-05 15:11:47 +02:00
Kenn JacobsenandGitHub 3caa43a5bf Fix root path regression for the Delivery API (#19216) 2025-05-03 15:26:20 +02:00
1efe860a89 V13: Clear Member Username Cache in Load Balanced Environments (#19191)
* Clear usernamekey

* Odd explaining comment

* Update src/Umbraco.Core/Cache/Refreshers/Implement/MemberCacheRefresher.cs

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

* Make UserNameCachePrefix readonly for better immutabilityly

* Move prefix to CacheKeys constants

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-04-29 18:03:28 +02:00
Andy Butland d49965549c Merge branch 'v13/contrib' into v13/dev 2025-04-29 14:26:00 +02:00
Dave WoestenborghsandGitHub 8d85259025 Upgrade examine to 3.7.1 (#19186) 2025-04-29 14:15:27 +02:00
Andreas ZerbstandGitHub 097d0456cc V13 QA updated pipeline for acceptance to avoid issue when installing playwright (#19140)
* Updated pipeline to install only the chromium browser

* Added junit as reporter for acceptance tests
2025-04-29 11:48:38 +00:00
mole a62fa93c77 Use windows agent for nuget push 2025-04-28 10:58:29 +02:00
Andy Butland a22dcf05f2 Merge branch 'v13/contrib' into v13/dev 2025-04-28 06:46:10 +02:00
afa6fa5ca2 attempted fix for Save and Preview button #15360 (#19138)
Co-authored-by: Navya Sinha <navya.sinha@method4.co.uk>
2025-04-25 21:28:46 +02:00
c6effef622 Handle file paths as not found in delivery API by route requests (#19063)
* Handle file paths as not found in delivery API by route requests.

* Move check earlier to handle redirect logic as well.

* Spelling: Changed "resolveable" to "resolvable"

---------

Co-authored-by: kjac <kja@umbraco.dk>
2025-04-22 06:46:33 +02:00
Andy Butland ab31fbb0aa Bump version to 13.8.1. 2025-04-17 11:14:17 +02:00
Andy Butland 7d9a2c6e70 Merge branch 'release/13.8.0' into v13/dev
# Conflicts:
#	version.json
2025-04-10 11:19:32 +02:00
Andy Butland a486d5df33 Bumped version to 13.8.0. 2025-04-10 07:38:20 +02:00
Andy Butland 3c8a621817 Merge branch 'v13/dev' into v13/contrib 2025-04-04 08:01:38 +02:00
Andy Butland 0e0aca55af Verify endpoint selection candidates with host attribute are ignored if request doesn't match the configured hosts. (#18820) 2025-04-04 06:54:56 +02:00
0f02584143 attempted fix for Datepicker v13 issue #16008 (#18903)
Co-authored-by: Navya Sinha <navya.sinha@method4.co.uk>
2025-04-04 03:52:48 +02:00
a3db45609a Move database cache rebuild to a background task with polling (13) (#18922)
* Converts rebuild database cache operation to submit and poll.

* Update src/Umbraco.Web.UI.Client/src/views/dashboard/settings/publishedsnapshotcache.controller.js

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

* Handle HTTP error in status retrieval.

* Fixed test build.

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-04-03 21:58:56 +02:00
Andy ButlandandGitHub d60a2a217c Verify endpoint selection candidates with host attribute are ignored if request doesn't match the configured hosts. (#18820) 2025-04-03 21:13:45 +02:00
Andy Butland 3e9ff6b5cb Check we have matched a preview URL by ID when exiting preview. (#18841) 2025-04-03 10:39:47 +02:00
Andy ButlandandGitHub 05a7d337de Check we have matched a preview URL by ID when exiting preview. (#18841) 2025-04-03 10:32:27 +02:00
Jacob OvergaardandGitHub fdca086a47 build(deps): bump @umbraco-ui/uui from 1.12.2 to 1.13.0 (#18830) 2025-03-26 14:32:29 +00:00
Andreas ZerbstandGitHub 42a81beeac V13 hotfix sqlserver integration tests (#18744)
* Disabled encrypt

* Skips integration tests for SQl Server on releases

* Removed encrypt
2025-03-20 11:59:03 +01:00
Andy Butland 9284b9e0b1 Make preview check for delivery API content case insensitive. (#18731) 2025-03-20 06:58:57 +01:00
Andy ButlandandGitHub eb91f4fef4 Make preview check for delivery API content case insensitive. (#18731) 2025-03-20 06:53:43 +01:00
Andy Butland 68acc2aa51 Bumped version to 13.9.0-rc. 2025-03-19 16:39:34 +01:00
Andy ButlandandGitHub 5570583f70 Fixes issue with macro rendering in an RTE when GUIDs are used for backoffice document routes (#18691)
* Fixes issue with macro rendering in an RTE when GUIDs are used for backoffice document routes.

* Fixed null reference error.
2025-03-18 11:10:23 +01:00
Andy ButlandandGitHub eb979625d1 Render folders before files in static files picker. (#18701) 2025-03-18 11:05:54 +01:00
Andy ButlandandGitHub e1e5f527e4 Updates dependency on Examine to 3.7. (#18676) 2025-03-17 09:13:09 +01:00
Lan Nguyen ThuyandJacob Overgaard f016dbd223 Fix issue text overflow when user name is too long 2025-03-13 11:07:57 +01:00
Andy Butland b2aa04ae1a Merge branch 'release/13.7.2' into v13/dev
# Conflicts:
#	version.json
2025-03-12 12:18:45 +01:00
Andy ButlandandGitHub e270adc50f Reverts UI updates from publish with desdendants dialog. (#18647) 2025-03-12 08:10:34 +01:00
Andy Butland fd5b8de533 Bumped version to 13.7.2 2025-03-12 07:25:35 +01:00
Andy Butland e3d9b042c2 Avoids collection was modified issue when flowing identities to the authenticated user's principal. (#18527) 2025-03-12 07:24:50 +01:00
Andy Butland f4ae796b30 Merge branch 'v13/dev' of https://github.com/umbraco/Umbraco-CMS into v13/dev 2025-03-11 09:18:25 +01:00
Andy Butland 367d0386b6 Merge branch 'release/13.7.1' into v13/dev
# Conflicts:
#	version.json
2025-03-11 09:18:12 +01:00
5b54bed406 Merge commit from fork
* Fixed parsing of node if in content and media permission querystring handlers to retrieve expected value when multiple are provided in the querystring.

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

* Bumped version to 13.6.1.

* 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
Anders BjernerandMichael Latouche d3b60277d7 Fixed typo in TinyMCE's da.js 2025-03-10 15:38:50 +01:00
twoday-rolandkockandAndy Butland 2ec6ff4ebc bumped imagesharp to prevent CVE-2025-27598 (#18602) 2025-03-09 08:43:52 +01:00
twoday-rolandkockandGitHub 78a8d017a2 bumped imagesharp to prevent CVE-2025-27598 (#18602) 2025-03-09 08:41:06 +01:00
1a205d8727 Handle multiline statements in migrations (#18478)
* Handle multiline statements in migrations

* Fixed failing unit tests and incorrect obsoletion.

* Fixed failing integration tests.

* Applied suggestion from code review.

---------

Co-authored-by: Sven Geusens <sge@umbraco.dk>
2025-03-04 17:27:33 +01:00
Andy ButlandandGitHub 396b5ea211 Avoids collection was modified issue when flowing identities to the authenticated user's principal. (#18527) 2025-03-04 12:12:29 +01:00
Andy ButlandandGitHub 3cf9099f56 Remove version from models builder generated code header when configured to do so. (#18501) 2025-03-04 11:52:20 +01:00
Andy Butland 451b5f96e7 Use windows for build agent to avoid hanging issues with Linux. 2025-02-28 06:57:35 +01:00
Andy Butland 15ebae5025 Bumped version to 13.7.1. 2025-02-28 06:57:00 +01:00
Andy Butland 32bfbadb8e Merge branch 'release/13.7.0' into v13/dev
# Conflicts:
#	version.json
2025-02-27 06:47:15 +01:00
Kenn JacobsenandGitHub 5ed09ebefa Backport ShowUnroutableContentWarnings to V13 (#18479) 2025-02-26 20:13:27 +01:00
Andy Butland eaea7a67ba Bump version to 13.7.0. 2025-02-26 08:14:26 +01:00
Andy ButlandandGitHub de2114b8c5 Adds warning to publish descendants dialog when force re-publish is selected. (#18409) 2025-02-22 09:39:58 +01:00
Andy ButlandandGitHub 64607ddbaa Disable webhook firing if disable in configuration. (#18383) 2025-02-19 12:17:04 +01:00
Andy ButlandandGitHub db1d999721 Avoid exception when attempting to find member by Id when Id is not an expected Guid or integer, as can be the case with external member providers. (#18320) 2025-02-17 12:47:52 +01:00
Andy ButlandandGitHub 18047a7cfb Only filter post retrieval of entities for start nodes if working with entities that support start nodes. (#18287) 2025-02-17 12:45:51 +01:00
Andy ButlandandGitHub def7ebd48c Html encodes the user's name in the invite email. (#18343) 2025-02-17 12:25:12 +01:00
8c2b1ebdc5 V13: Introduce publishNotifications method on IMembershipMemberService (#18207)
* Introduce publishNotifications method on IMembershipMemberService.cs

* Fix test

* Add PublishNotificationSaveOptions

* Fix up according to comments

* Use numeric values for flag enum

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

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

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

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

---------

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

* Added integration task verifying updated behaviour.

* Variant integration test.

* Update test data controller.

* Remove usued function parameters.

* Refactor to enum.

* Fixed flags enum.

* Variable name refactor.

* Applied changes from code review.

* Refactored method name.

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

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

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

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

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

* Aligned default values on security settings.

* Added validator for security settings.

* Provide default implementation for get members by email.

* Refactored constructor of MemberController.

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

* Further code tidy and use of DI in constructor.

* Used new constructor in tests.

* Add unit test for modified behaviour.

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

* Spelling.

---------

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

* Cache null values in DictionaryByKeyRepository

* Fixed issue with nullable reference.

* Updated logic for caching of null values.

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

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

* Made the NullValueRepresentation overwritable in a generic manner

* Improve generic NullValueCachePolicyResolver

* Revert Commits and clarify logic with comment

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

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

---------

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

* Amend exception and fix typo

* Minor tweak to comment and exception message.

---------

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

* Add extension to more easily get member identity

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

* Update snippet

* Fix the comment that I broke

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

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

---------

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

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

* Do not checkout again

* More test pipeline

* Another attempt

* yet another attempt

* more attempts

* Revert "more attempts"

This reverts commit 5694d97ba6.

* Test without building backoffice and login explicitly

* Fix mem leak in integration tests

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

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

* Simplify the fix :)

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

* Add Package Reference

---------

Co-authored-by: Sebastiaan Janssen <sebastiaan@umbraco.com>
2025-01-20 11:45:35 +01:00
Callum WhyteandAndy Butland 7552e315fb Allow skipSelect blueprints only when one blueprint exists (#17818) 2025-01-20 11:45:26 +01:00
Callum WhyteandGitHub d15b5b221e Allow skipSelect blueprints only when one blueprint exists (#17818) 2025-01-20 10:27:54 +01:00
60e3fa97a9 Lucene Package Update to Address CVE-2024-43383 (#17942)
* Update Lucene Package to 4.8.0-beta00017

* Add Package Reference

---------

Co-authored-by: Sebastiaan Janssen <sebastiaan@umbraco.com>
2025-01-20 10:04:02 +01:00
Sven Geusens fd1bd51aff bump version 2025-01-16 10:10:04 +01:00
Sven Geusens b12620af9c Merge branch 'release/13.6' into v13/dev 2025-01-16 10:08:43 +01:00
Sven Geusens d1af59fdf0 Merge branch 'v13/dev' into release/13.6 2025-01-15 22:13:42 +01:00
Andreas ZerbstandGitHub 1b050ebc47 V13 QA added parallelization for E2E tests (#17995)
* Added script for running tests

* Added parallelization for our E2E tests

* Removed line

* Removed condition
2025-01-15 22:12:45 +01:00
Sven Geusens ff3e457c58 Merge branch 'v13/dev' into release/13.6 2025-01-15 20:55:41 +01:00
Andy ButlandandGitHub 2ffaede07b Added a maxlength setting to the regular expression pattern field. (#17982) 2025-01-15 20:54:54 +01:00
Sven GeusensandGitHub 2a83f57262 Do not re-map schedule modal on error as it leads to broken bindings (#17990) 2025-01-15 17:35:23 +01:00
Kenn JacobsenandGitHub 5be9193978 Do not show "Save and preview" by default (#17986) 2025-01-15 15:42:37 +01:00
Niels LyngsøandSven Geusens d3f264fc54 Fix: only fire event when instance is available (issue 17847) (#17987)
* only fire event when instance is available

* avoid js issue when no markup
2025-01-15 14:01:33 +01:00
Niels LyngsøandGitHub 04ea002bf3 Fix: only fire event when instance is available (issue 17847) (#17987)
* only fire event when instance is available

* avoid js issue when no markup
2025-01-15 13:56:53 +01:00
Kenn JacobsenandGitHub 8f42a3a0ea Make the RTE handle inline elements gracefully for JSON (#17983) 2025-01-15 11:26:03 +01:00
Kenn JacobsenandGitHub 4dd8fbe25d Enforce the "Save and preview" button after when switching content app (#17985) 2025-01-15 09:55:00 +01:00
Sven GeusensandGitHub ee727c43e1 Added localizations for composite key of missing New (media) Audit item (#17981) 2025-01-14 21:08:47 +01:00
Jacob OvergaardandGitHub 395213946b build(deps): update @microsoft/signalr from 7.0.12 to 8.0.7 (#17973) 2025-01-14 15:44:52 +00:00
Andy ButlandandGitHub ed0b236208 Resets send to approval dialog on close such that when re-opening the selection and operation can still be triggered. (#17956) 2025-01-14 16:25:26 +01:00
Kenn JacobsenandGitHub 8d56e75257 Add Vary header to all content API operations (#17962)
* Add Vary header to all content API operations

* Make attribute sealed
2025-01-14 15:54:19 +01:00
Jacob OvergaardandGitHub b3c27415d1 build(deps): bump @umbraco-ui/uui from 1.11.0 to 1.12.2 (#17972) 2025-01-14 15:48:34 +01:00
Justin NevilleandNikolaj Geisle 9839b0ee66 Fixes issue with the IVirtualPageController not setting PublishedRequest content via FindContent (#15121)
* Fixes issue https://github.com/umbraco/Umbraco-CMS/issues/12834

* Re-worked based on feedback

* Use dependency injection

* Only call new constructor

---------

Co-authored-by: Nikolaj Geisle <70372949+Zeegaan@users.noreply.github.com>
(cherry picked from commit fd9c1a0e9c)
2025-01-14 13:57:00 +01:00
f3f57f9748 Improve YSOD rendering in the client (#17940)
* Conditionally render error details and make sure something is rendered

* V13 QA updated locator for failing E2E test (#17825)

* Added timeouts

* Nullcheck user in Content permission handlers (#17846)

---------

Co-authored-by: Sven Geusens <sge@umbraco.dk>
Co-authored-by: Niels Lyngsø <nsl@umbraco.dk>

---------

Co-authored-by: Andreas Zerbst <73799582+andr317c@users.noreply.github.com>
Co-authored-by: Niels Lyngsø <nsl@umbraco.dk>
2025-01-14 13:38:07 +01:00
Nikolaj GeisleandGitHub 85d16a4354 V13: Show validation error, when saving invalid media (#17961)
* Don't save invalid media

* Revert "Don't save invalid media"

This reverts commit c4b6acf542.

* Still allow save, but show validation error, not popup error

* Add clarifying comment
2025-01-14 11:18:36 +01:00
Kenn JacobsenandGitHub c0517c3562 Allow filtering for multiple content types (#17960) 2025-01-14 10:40:45 +01:00
92e535a1c9 V13 QA updated locator for failing E2E test (#17825)
* Added timeouts

* Nullcheck user in Content permission handlers (#17846)

---------

Co-authored-by: Sven Geusens <sge@umbraco.dk>
Co-authored-by: Niels Lyngsø <nsl@umbraco.dk>
2025-01-14 09:12:41 +01:00
Kenn JacobsenandGitHub 7411c2552b Added integration test to verify the Delivery API contract (#17949) 2025-01-13 15:15:37 +01:00
Andy ButlandandGitHub f2410f376e Corrected property names and structure used in JSON schema generation for configuration of default data to install. (#17946) 2025-01-13 11:05:51 +01:00
Kenn JacobsenandGitHub a9313c1367 Make the Delivery API "ancestors" selector work with preview (#17938) 2025-01-13 07:47:14 +01:00
Nikolaj Geisleandkjac 5b3b9d770a V13: Add rcl files to static files tree (#17671)
* Refactor to use IWebhostEnvironment for wwwroot files.

* Obsolete ctor

* Add ActivatorUtiiliesConstructor

* Refactor logic to include folders

* Fix so we can see files in wwwroot/App_Plugins

* Cosmetic: Use invariant comparison for the "umbraco" folder".

---------

Co-authored-by: kjac <kja@umbraco.dk>
2025-01-09 10:02:34 +01:00
4a54215fa1 V13: Add rcl files to static files tree (#17671)
* Refactor to use IWebhostEnvironment for wwwroot files.

* Obsolete ctor

* Add ActivatorUtiiliesConstructor

* Refactor logic to include folders

* Fix so we can see files in wwwroot/App_Plugins

* Cosmetic: Use invariant comparison for the "umbraco" folder".

---------

Co-authored-by: kjac <kja@umbraco.dk>
2025-01-09 09:36:32 +01:00
Sven Geusensandnikolajlauridsen 52fbb34ddd Nullcheck user in Content permission handlers (#17846) 2025-01-07 15:07:42 +01:00
Sven GeusensandGitHub c96dc7eaa1 Nullcheck user in Content permission handlers (#17846) 2025-01-07 14:52:40 +01:00
Jacob Overgaard f19409ba5b Revert "bump version"
This reverts commit 679c42ef90.
2025-01-06 12:51:10 +01:00
Jacob Overgaard 679c42ef90 bump version 2025-01-06 12:46:03 +01:00
Elitsa MarinovskaandGitHub 3304c6f472 Enable anonymous calls to HandleLogout action (#17807) 2024-12-16 07:26:12 +01:00
Nikolaj GeisleandGitHub 34f09fa260 V13: Clear username cache (#17815)
* Clear member username cache on delete

* Also refresh cache on update
2024-12-16 08:18:30 +02:00
Jacob Overgaard fd4efd38cd Merge branch 'v13/dev' into release/13.6 2024-12-13 10:07:46 +01:00
Jacob Overgaard e4e9bfc3e9 bump version to 13.6.0-rc2 2024-12-13 09:58:13 +01:00
Jacob Overgaard 3774dbe38c Merge remote-tracking branch 'origin/v13/contrib' into v13/dev 2024-12-13 09:56:46 +01:00
Dan ListerandJacob Overgaard 4ef50642ad Fixed an issue where bold markdown would not work inside a property description 2024-12-13 09:53:36 +01:00
Bjarke BergandGitHub f95fb78022 Merge pull request #17800 from umbraco/v13/bugfix/corrupt-examine-dashboard
Make sure Examine dashboard still functions when an index is corrupt
2024-12-12 12:07:06 +01:00
Sven Geusens 0f9a8bde61 Broadend the error catching when building the examine dashboard 2024-12-12 09:38:35 +01:00
Sven Geusens 129d369c1a Version bump to release 2024-12-11 13:46:38 +01:00
Sven Geusens dd2c591a5b Merge branch 'release/13.6' into v13/dev 2024-12-11 13:45:35 +01:00
Kenn JacobsenandNikolaj Geisle 16749a724d Add (un)publishing details to TreeChange notifications (#17757)
(cherry picked from commit 404a62aa0b)
2024-12-09 11:42:53 +01:00
Kenn JacobsenandNikolaj Geisle a627930b58 Warn about un-routable content at publish time (#17705)
(cherry picked from commit 2d9cfc880b)
2024-12-09 11:42:46 +01:00
Kenn JacobsenandGitHub 404a62aa0b Add (un)publishing details to TreeChange notifications (#17757) 2024-12-09 11:36:48 +01:00
Kenn JacobsenandGitHub 2d9cfc880b Warn about un-routable content at publish time (#17705) 2024-12-09 11:07:32 +01:00
Kenn JacobsenandGitHub ee8bdfc4ac Add "user info" endpoint for the Delivery API (#17719)
* Add "user info" endpoint for the Delivery API

* Add comment about public and unsealed class
2024-12-09 09:57:21 +01:00
Bjarke BergandGitHub b55d484863 Updated NuGet Packages for v13 (#17704)
* Update nuget packages

* Cherry pick fixes from v15

* Fixed: Projects that use central package version management should not define the version on the PackageReference items but on the PackageVersion items

* rollback update in tool
2024-12-03 08:51:23 +01:00
Ronald BarendseandGitHub ed0058a503 Fix PanicException: failed to get child with id= after updating content types (#17702)
* Remove tree node from hierarchy after clearing branch

* Refactor ClearBranchLocked to split clearing child and siblings
2024-12-03 08:00:18 +01:00
Jacob OvergaardandGitHub b0aed393d6 fix: keep the lead char when editing an existing link (#17667) 2024-11-28 14:04:59 +00:00
Sven Geusens a1ea203281 Merge branch 'v10/dev' into v13/dev
# Conflicts:
#	src/Directory.Packages.props
#	src/Umbraco.Infrastructure/Manifest/ManifestParser.cs
2024-11-26 21:08:22 +01:00
Kenn JacobsenandGitHub 4590739fa5 Add ASCII file name conversion (#17580) 2024-11-26 11:26:44 +01:00
7c617f2976 Revert #14234 add update error message for DB connection failures (#17612)
* Revert #14234

* Make the boot failure message more descriptive when unable to connect to DB

* Update src/Umbraco.Infrastructure/Runtime/RuntimeState.cs

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

* Revert changes

* Obsolete InstallMissingDatabase from V16

---------

Co-authored-by: Ronald Barendse <ronald@barend.se>
2024-11-26 11:06:40 +01:00
Lee KelleherandGitHub 9141f61708 V13: Dropzone, upload complete callback with processed file array (#17631)
* Dropzone, upload complete callback with processed file array

* Media card: cosmetic fix for image border-radius

The image's square corners were poking out.
2024-11-25 15:51:16 +01:00
ba4120050f Add cache key to GetByUserName (#17350)
* Add cache key to GetByUserName

* Remove constants

* create new cache policy for member repository

---------

Co-authored-by: Elitsa <elm@umbraco.dk>
2024-11-25 15:27:22 +01:00
Kenn JacobsenandGitHub cbd4dc6e0d Handle "all slashes" routes (#17596) 2024-11-22 09:06:10 +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
Sven Geusensandnikolajlauridsen 9febbc7db1 Distinguish between default zero and intentional zero sort order for new documents (#17517)
* Distinguish between default value and initial zero

* Update special value comment documentation

* Redid solution with dirty/new entity tracking

* rework copy branch sortorder fix

* Change == false to is false

---------

Co-authored-by: Mole <nikolajlauridsen@protonmail.ch>
2024-11-21 09:09:25 +01:00
Kenn JacobsenandGitHub ec8e10f406 Fix RTE console error when blocks are not available (#17582) 2024-11-21 08:53:59 +01:00
EalseandJacob Overgaard 3233b24a4e fix: infinite editor stuck in create action (#15945) 2024-11-20 10:00:50 +01:00
Anders BjernerandJacob Overgaard 5775e9cb18 Added fix for <umb-button-group /> label issues 2024-11-20 09:59:27 +01:00
MoleandGitHub 570005f5e1 Run both cms and package migrations in upgrader (#17575)
* Run both cms and package migrations in upgrader

* Use correct setting
2024-11-20 08:38:33 +01:00
Bjarne FyrstenborgandLaura Neto fbaa3440f2 Don't shrink crops container on right side 2024-11-18 20:11:48 +01:00
Bjarne FyrstenborgandJacob Overgaard e662468ecc Set TinyMCE to readonly 2024-11-18 08:51:04 +01:00
Bjarne FyrstenborgandMatthew-Wise c8efc30654 Fallback value when setttings element type doesn't exists as it can have reference, but no available element type 2024-11-17 17:05:28 +00:00
marwaldvandJacob Overgaard 79d117026f Add swiss languages as backoffice languages
Add missing backoffice languages as properties etc. are translated via translation section (e.g. with "#Backoffice Settings"). At the moment the swiss languages always need to be added via the umbraco/Config folder.
2024-11-09 12:29:44 +01:00
Casper LundgreenandGitHub 2709054bdb Fix NameIdentifier is not an integer. (TryParse implemented) (#17414)
Issue: 17383
2024-11-08 13:27:53 +01:00
MoleandGitHub 2d4230c001 Include create date in audit item (#17447) 2024-11-08 08:58:38 +01:00
Zeegaan f7729508cf Merge branch 'release/13.5' into v13/dev
# Conflicts:
#	version.json
2024-10-23 08:57:30 +02: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
Andreas ZerbstandGitHub aa1f3df76b Updated to match locator (#17334) 2024-10-23 08:37:24 +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 8a22672c7f Updated version of messagepack (#17321) 2024-10-21 11:15:59 +02:00
Andreas ZerbstandGitHub 11270eaaf5 Updated message pack (#17320) 2024-10-21 11:00:47 +02:00
Justin NevilleandGitHub 1e32d59ecb Query for media using the full file path as well as the original file path for files that contain the sizes in the file name (i.e. image_200x200.jpg) (#17314) 2024-10-21 09:53:17 +02:00
Kenn JacobsenandGitHub eab27123dd MNTP: Re-initialize contextual dialog options upon content creation (#17301) 2024-10-18 09:39:29 +02:00
Kenn JacobsenandGitHub a6253957c8 MNTP: Improve site and root context for dynamic root (#17303) 2024-10-18 09:17:17 +02:00
Jacob OvergaardandGitHub a8f5631144 V13: Update @umbraco-ui/uui to 1.11.0 (#17281)
* build(deps): update @umbraco-ui/uui from 1.7.1 to 1.11.0

* fix: umb-login-element no attributes

Cherry-picked a fix from V14 where the custom login input component was no longer needed, which was fixed because it errors out. This simplifies the login form.

* cherry-pick code to handle 'enter' click from v14
2024-10-17 10:25:02 +01:00
Jason ElkinandGitHub ee37ad0f4b Unlock form after unsuccessful save and publish. (#17285) 2024-10-16 15:08:37 +02:00
Bjarke BergandGitHub 2d71b5a63b Updated image sharp to a non vulnerable version (#17290) 2024-10-16 12:16:38 +02:00
Ronald BarendseandGitHub c3db3457e7 Fix ContentStore locking exceptions in async code (#17246)
* Add ContentCache test

* Use SemaphoreSlim as write lock

* Apply lock imrpovements to SnapDictionary

* Obsolete unused MonitorLock
2024-10-11 09:45:01 +02:00
Sven GeusensandGitHub 7787af2df1 Fix install url detection (#17241) 2024-10-10 18:09:11 +02:00
Wojciech WróbelandJacob Overgaard 3730109cce Fixed issue #17225. The 'code' html element added as valid to RTE configuration 2024-10-09 14:19:38 +02:00
Nikolaj Geisle 9bab74d30e Bump version.json 2024-10-07 11:55:34 +02:00
Bjarke Berg 63a487bc0b Merge remote-tracking branch 'origin/v13/dev' into v13/dev 2024-09-26 07:54:43 +02:00
Ronald BarendseandBjarke Berg a40eadcfce Add RemoveDefault() extension method to fluent API for CMS webhook events (#15424)
* Add RemoveDefault extension method

* Move default webhook event types to single list

(cherry picked from commit 8f26263178)
2024-09-26 07:54:26 +02:00
9b19d63a6a update ImageSharpMiddlewareOption for fixing invalid width and height (#17126)
Co-authored-by: Lan Nguyen Thuy <lnt@umbraco.dk>
2024-09-25 13:19:09 +02:00
Terence BurridgeandSebastiaan Janssen 6939472f37 Update valid reasons not to have a template on a content node to include having a redirect field
Cherry-picked from: 385a5345b1
2024-09-23 14:30:13 +02:00
Sebastiaan Janssen 0387307301 Merge branch 'v13/dev' into v13/contrib 2024-09-23 14:27:48 +02:00
Bjarke Berg 25a5dde8e9 Merge remote-tracking branch 'origin/release/13.5' into v13/dev
# Conflicts:
#	version.json
2024-09-18 12:55:35 +02:00
Sebastiaan Janssen ff9903b14d Merge branch 'v13/contrib' of https://github.com/umbraco/Umbraco-CMS into v13/contrib 2024-09-17 13:58:37 +02:00
Dennis ÖhmanandGitHub d64bf5de22 v13.5 - New Swedish translation crashes Umbraco, removed duplicate areas. (#17059) 2024-09-17 13:56:58 +02:00
Sebastiaan Janssen 9300d867fb Merge branch 'v13/dev' into v13/contrib 2024-09-17 13:12:04 +02:00
Sven Geusens b237285703 Merge branch 'release/13.5' into v13/dev and version bump 2024-09-12 15:12:20 +02:00
Sven GeusensandGitHub a74d963cfa Fix null reference exception in CacheValues.For when building the CompositeStringStringKey (#17024)
* Fix null ref exeption based on IPropertyValue.Culture documentation

* Clarify comment
2024-09-09 18:13:14 +02:00
Yari MariënandGitHub 5fe18bb78c Content.EditorDirectiveController: added formSubmittedValidationFailed broadcast to prevent fields to stay disabled (#17018) 2024-09-09 13:05:12 +02:00
Niels LyngsøandGitHub b16dfa9ca1 dispatch change event when embedded media has been added (#17008) 2024-09-09 12:49:07 +02:00
3e6116fcba No longer shows success message if content moving is cancelled (#15051)
* Fix for issue https://github.com/umbraco/Umbraco-CMS/issues/13923
 - Added AttemptMove method to the ContentService
 - Updated ContentController PostMove method to return ValidationProblem whenever the node is not moved

* Align changes with V14 solution. Make it non breaking.

---------

Co-authored-by: Laura Neto <12862535+lauraneto@users.noreply.github.com>
2024-09-05 14:08:48 +02:00
Ronald BarendseandGitHub ef3bf496e9 Avoid concurrent build of Umbraco.JsonSchema tool and add execution timeouts to Exec build tasks (#17006)
* Disable building Umbraco.JsonSchema and Umbraco.Tests.AcceptanceTest.UmbracoProject

* Add 10 minute timeout to Exec MSBuild tasks
2024-09-04 13:44:19 +02:00
Sebastiaan Janssen bff293213d Merge branch 'v13/dev' into v13/contrib 2024-09-03 11:01:39 +02:00
a47a1775f2 Prevent templates being editable when using Production runtime mode (#16923)
Co-authored-by: Nick Hoang <nick@codeberwery.com>
Co-authored-by: Jason Elkin <jason@bumpdigital.co.uk>
2024-09-03 00:54:32 +01:00
Jason Elkin b2a2df7a8a Ignore Visual Studio's generated launchSettings file. 2024-08-28 14:08:52 +01:00
Sven Geusens 34179f5b7d Merge branch 'release/13.5' into v13/dev and bump version 2024-08-28 10:14:09 +02:00
268 changed files with 18659 additions and 3867 deletions
+1 -1
View File
@@ -35,7 +35,7 @@
<EnableStrictModeForCompatibleTfms>true</EnableStrictModeForCompatibleTfms>
</PropertyGroup>
<!-- Calculate version only once for the whole repository -->
<!-- Calculate version only once for the whole repository -->
<PropertyGroup>
<GitVersionBaseDirectory>$(MSBuildThisFileDirectory)</GitVersionBaseDirectory>
</PropertyGroup>
+31 -26
View File
@@ -12,28 +12,28 @@
</ItemGroup>
<!-- Microsoft packages -->
<ItemGroup>
<PackageVersion Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="8.0.8" />
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation" Version="8.0.8" />
<PackageVersion Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="8.0.11" />
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation" Version="8.0.11" />
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="4.10.0" />
<PackageVersion Include="Microsoft.Data.Sqlite" Version="8.0.8" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.8" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.SqlServer" Version="8.0.8" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.8" />
<PackageVersion Include="Microsoft.Data.Sqlite" Version="8.0.11" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.11" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.SqlServer" Version="8.0.11" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.11" />
<PackageVersion Include="Microsoft.Extensions.Caching.Abstractions" Version="8.0.0" />
<PackageVersion Include="Microsoft.Extensions.Configuration.Abstractions" Version="8.0.0" />
<PackageVersion Include="Microsoft.Extensions.Configuration.Json" Version="8.0.0" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0" />
<PackageVersion Include="Microsoft.Extensions.FileProviders.Embedded" Version="8.0.8" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="8.0.1" />
<PackageVersion Include="Microsoft.Extensions.FileProviders.Embedded" Version="8.0.11" />
<PackageVersion Include="Microsoft.Extensions.FileProviders.Physical" Version="8.0.0" />
<PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="8.0.0" />
<PackageVersion Include="Microsoft.Extensions.Http" Version="8.0.0" />
<PackageVersion Include="Microsoft.Extensions.Identity.Core" Version="8.0.8" />
<PackageVersion Include="Microsoft.Extensions.Identity.Stores" Version="8.0.8" />
<PackageVersion Include="Microsoft.Extensions.Logging" Version="8.0.0" />
<PackageVersion Include="Microsoft.Extensions.Http" Version="8.0.1" />
<PackageVersion Include="Microsoft.Extensions.Identity.Core" Version="8.0.11" />
<PackageVersion Include="Microsoft.Extensions.Identity.Stores" Version="8.0.11" />
<PackageVersion Include="Microsoft.Extensions.Logging" Version="8.0.1" />
<PackageVersion Include="Microsoft.Extensions.Options" Version="8.0.2" />
<PackageVersion Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="8.0.0" />
<PackageVersion Include="Microsoft.Extensions.Options.DataAnnotations" Version="8.0.0" />
<PackageVersion Include="System.Runtime.Caching" Version="8.0.0" />
<PackageVersion Include="System.Runtime.Caching" Version="8.0.1" />
</ItemGroup>
<!-- Umbraco packages -->
<ItemGroup>
@@ -45,13 +45,13 @@
<PackageVersion Include="Asp.Versioning.Mvc" Version="7.1.1" />
<PackageVersion Include="Asp.Versioning.Mvc.ApiExplorer" Version="7.1.0" />
<PackageVersion Include="Dazinator.Extensions.FileProviders" Version="2.0.0" />
<PackageVersion Include="Examine" Version="3.3.0" />
<PackageVersion Include="Examine.Core" Version="3.3.0" />
<PackageVersion Include="HtmlAgilityPack" Version="1.11.64" />
<PackageVersion Include="Examine" Version="3.7.1" />
<PackageVersion Include="Examine.Core" Version="3.7.1" />
<PackageVersion Include="HtmlAgilityPack" Version="1.11.71" />
<PackageVersion Include="K4os.Compression.LZ4" Version="1.3.8" />
<PackageVersion Include="MailKit" Version="4.7.1.1" />
<PackageVersion Include="MailKit" Version="4.8.0" />
<PackageVersion Include="Markdown" Version="2.2.1" />
<PackageVersion Include="MessagePack" Version="2.5.187" />
<PackageVersion Include="MessagePack" Version="2.5.192" />
<PackageVersion Include="MiniProfiler.AspNetCore.Mvc" Version="4.3.8" />
<PackageVersion Include="MiniProfiler.Shared" Version="4.3.8" />
<PackageVersion Include="ncrontab" Version="3.3.3" />
@@ -62,37 +62,42 @@
<PackageVersion Include="OpenIddict.AspNetCore" Version="4.10.1" />
<PackageVersion Include="OpenIddict.EntityFrameworkCore" Version="4.10.1" />
<PackageVersion Include="Serilog" Version="3.1.1" />
<PackageVersion Include="Serilog.AspNetCore" Version="8.0.2" />
<PackageVersion Include="Serilog.AspNetCore" Version="8.0.3" />
<PackageVersion Include="Serilog.Enrichers.Process" Version="2.0.2" />
<PackageVersion Include="Serilog.Enrichers.Thread" Version="3.1.0" />
<PackageVersion Include="Serilog.Expressions" Version="4.0.0" />
<PackageVersion Include="Serilog.Extensions.Hosting" Version="8.0.0" />
<PackageVersion Include="Serilog.Formatting.Compact" Version="2.0.0" />
<PackageVersion Include="Serilog.Formatting.Compact.Reader" Version="3.0.0" />
<PackageVersion Include="Serilog.Settings.Configuration" Version="8.0.2" />
<PackageVersion Include="Serilog.Settings.Configuration" Version="8.0.4" />
<PackageVersion Include="Serilog.Sinks.Async" Version="1.5.0" />
<PackageVersion Include="Serilog.Sinks.File" Version="5.0.0" />
<PackageVersion Include="Serilog.Sinks.Map" Version="1.0.2" />
<PackageVersion Include="SixLabors.ImageSharp" Version="3.1.5" />
<PackageVersion Include="SixLabors.ImageSharp" Version="3.1.7" />
<PackageVersion Include="SixLabors.ImageSharp.Web" Version="3.1.3" />
<PackageVersion Include="Smidge.InMemory" Version="4.4.0" />
<PackageVersion Include="Smidge.Nuglify" Version="4.4.0" />
<PackageVersion Include="Swashbuckle.AspNetCore" Version="6.7.1" />
<PackageVersion Include="Smidge.Nuglify" Version="4.5.1" />
<PackageVersion Include="Swashbuckle.AspNetCore" Version="6.9.0" />
</ItemGroup>
<!-- Transitive pinned versions (only required because our direct dependencies have vulnerable versions of transitive dependencies) -->
<ItemGroup>
<!-- Both Microsoft.EntityFrameworkCore.SqlServer and NPoco.SqlServer bring in a vulnerable version of Azure.Identity -->
<PackageVersion Include="Azure.Identity" Version="1.12.0" />
<PackageVersion Include="Azure.Identity" Version="1.13.1" />
<!-- Dazinator.Extensions.FileProviders brings in a vulnerable version of System.Net.Http -->
<PackageVersion Include="System.Net.Http" Version="4.3.4" />
<!-- Examine brings in a vulnerable version of System.Security.Cryptography.Xml -->
<PackageVersion Include="System.Security.Cryptography.Xml" Version="8.0.1" />
<PackageVersion Include="System.Security.Cryptography.Xml" Version="8.0.2" />
<!-- Both Dazinator.Extensions.FileProviders and MiniProfiler.AspNetCore.Mvc bring in a vulnerable version of System.Text.RegularExpressions -->
<PackageVersion Include="System.Text.RegularExpressions" Version="4.3.1" />
<!-- Both OpenIddict.AspNetCore, Npoco.SqlServer and Microsoft.EntityFrameworkCore.SqlServer bring in a vulnerable version of Microsoft.IdentityModel.JsonWebTokens -->
<PackageVersion Include="Microsoft.IdentityModel.JsonWebTokens" Version="7.7.1" />
<!-- Examine.Lucene bring in a vulnerable version of Lucene.Net.Replicator -->
<PackageVersion Include="Lucene.Net.Replicator" Version="4.8.0-beta00017" />
<!-- Both OpenIddict.AspNetCore, Microsoft.EntityFrameworkCore.* bring in a vulnerable version of Microsoft.Extensions.Caching.Memory -->
<PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="8.0.1" />
<!-- Both Azure.Identity, Microsoft.EntityFrameworkCore.SqlServer,NPoco.SqlServer, and more bring in a vulnerable version of System.Text.Json -->
<PackageVersion Include="System.Text.Json" Version="8.0.5" />
<!-- Both Microsoft.EntityFrameworkCore.SqlServer and NPoco.SqlServer bring in a vulnerable version of Microsoft.Data.SqlClient -->
<PackageVersion Include="Microsoft.Data.SqlClient" Version="5.2.2" />
</ItemGroup>
</Project>
+100 -24
View File
@@ -71,8 +71,12 @@ stages:
- job: A
displayName: Build Umbraco CMS
pool:
vmImage: 'ubuntu-latest'
vmImage: 'windows-latest'
steps:
- checkout: self
submodules: false
lfs: false,
fetchDepth: 500
- task: NodeTool@0
displayName: Use Node.js $(nodeVersion)
retryCountOnTaskFailure: 3
@@ -198,6 +202,11 @@ stages:
pool:
vmImage: 'ubuntu-latest'
steps:
- checkout: self
submodules: false
lfs: false,
fetchDepth: 1
fetchFilter: tree:0
- task: NodeTool@0
displayName: Use Node.js 10.15.x
retryCountOnTaskFailure: 3
@@ -249,6 +258,11 @@ stages:
pool:
vmImage: $(vmImage)
steps:
- checkout: self
submodules: false
lfs: false,
fetchDepth: 1
fetchFilter: tree:0
- task: DownloadPipelineArtifact@2
displayName: Download build artifacts
inputs:
@@ -288,6 +302,11 @@ stages:
variables:
Tests__Database__DatabaseType: 'Sqlite'
steps:
- checkout: self
submodules: false
lfs: false,
fetchDepth: 1
fetchFilter: tree:0
# Setup test environment
- task: DownloadPipelineArtifact@2
displayName: Download build artifacts
@@ -319,7 +338,9 @@ stages:
# Integration Tests (SQL Server)
- job:
timeoutInMinutes: 120
condition: or(eq(stageDependencies.Build.A.outputs['build.NBGV_PublicRelease'], 'True'), ${{parameters.sqlServerIntegrationTests}})
# We are currently encountering issues when running SQL Server Linux tests Microsoft.Data.SqlClient.SqlException (0x80131904)
# condition: or(eq(stageDependencies.Build.A.outputs['build.NBGV_PublicRelease'], 'True'), ${{parameters.sqlServerIntegrationTests}})
condition: eq(${{parameters.sqlServerIntegrationTests}}, True)
displayName: Integration Tests (SQL Server)
strategy:
matrix:
@@ -415,10 +436,24 @@ stages:
CONNECTIONSTRINGS__UMBRACODBDSN_PROVIDERNAME: Microsoft.Data.Sqlite
strategy:
matrix:
Linux:
vmImage: 'ubuntu-latest'
Windows:
vmImage: 'windows-latest'
LinuxPart1Of3:
vmImage: "ubuntu-latest"
testCommand: "npm run test -- --shard=1/3"
LinuxPart2Of3:
vmImage: "ubuntu-latest"
testCommand: "npm run test -- --shard=2/3"
LinuxPart3Of3:
vmImage: "ubuntu-latest"
testCommand: "npm run test -- --shard=3/3"
WindowsPart1Of3:
vmImage: "windows-latest"
testCommand: "npm run test -- --shard=1/3"
WindowsPart2Of3:
vmImage: "windows-latest"
testCommand: "npm run test -- --shard=2/3"
WindowsPart3Of3:
vmImage: "windows-latest"
testCommand: "npm run test -- --shard=3/3"
pool:
vmImage: $(vmImage)
steps:
@@ -496,12 +531,12 @@ stages:
workingDirectory: tests/Umbraco.Tests.AcceptanceTest
# Install Playwright and dependencies
- pwsh: npx playwright install --with-deps
displayName: Install Playwright
- pwsh: npx playwright install chromium
displayName: Install Playwright only with Chromium browser
workingDirectory: tests/Umbraco.Tests.AcceptanceTest
# Test
- pwsh: npm run test --ignore-certificate-errors
- pwsh: $(testCommand)
displayName: Run Playwright tests
continueOnError: true
workingDirectory: tests/Umbraco.Tests.AcceptanceTest
@@ -522,18 +557,28 @@ stages:
# Copy artifacts
- pwsh: |
if (Test-Path tests/Umbraco.Tests.AcceptanceTest/results/*) {
Copy-Item tests/Umbraco.Tests.AcceptanceTest/results $(Build.ArtifactStagingDirectory) -Recurse
Copy-Item tests/Umbraco.Tests.AcceptanceTest/results/* $(Build.ArtifactStagingDirectory) -Recurse
}
displayName: Copy Playwright results
condition: succeededOrFailed()
# Publish
# Publish test artifacts
- task: PublishPipelineArtifact@1
displayName: Publish test artifacts
condition: succeededOrFailed()
inputs:
targetPath: $(Build.ArtifactStagingDirectory)
artifact: 'Acceptance Tests - $(Agent.JobName) - Attempt #$(System.JobAttempt)'
artifact: "Acceptance Test Results - $(Agent.JobName) - Attempt #$(System.JobAttempt)"
# Publish test results
- task: PublishTestResults@2
displayName: "Publish test results"
condition: succeededOrFailed()
inputs:
testResultsFormat: 'JUnit'
testResultsFiles: '*.xml'
searchFolder: "tests/Umbraco.Tests.AcceptanceTest/results"
testRunTitle: "$(Agent.JobName)"
- job:
displayName: E2E Tests (SQL Server)
@@ -544,12 +589,30 @@ stages:
CONNECTIONSTRINGS__UMBRACODBDSN_PROVIDERNAME: Microsoft.Data.SqlClient
strategy:
matrix:
Linux:
vmImage: 'ubuntu-latest'
LinuxPart1Of3:
testCommand: "npm run testSqlite -- --shard=1/3"
vmImage: "ubuntu-latest"
SA_PASSWORD: $(UMBRACO__CMS__UNATTENDED__UNATTENDEDUSERPASSWORD)
CONNECTIONSTRINGS__UMBRACODBDSN: 'Server=(local);Database=Umbraco;User Id=sa;Password=$(SA_PASSWORD);TrustServerCertificate=True'
Windows:
vmImage: 'windows-latest'
CONNECTIONSTRINGS__UMBRACODBDSN: "Server=(local);Database=Umbraco;User Id=sa;Password=$(SA_PASSWORD);TrustServerCertificate=True"
LinuxPart2Of3:
testCommand: "npm run testSqlite -- --shard=2/3"
vmImage: "ubuntu-latest"
SA_PASSWORD: $(UMBRACO__CMS__UNATTENDED__UNATTENDEDUSERPASSWORD)
CONNECTIONSTRINGS__UMBRACODBDSN: "Server=(local);Database=Umbraco;User Id=sa;Password=$(SA_PASSWORD);TrustServerCertificate=True"
LinuxPart3Of3:
testCommand: "npm run testSqlite -- --shard=3/3"
vmImage: "ubuntu-latest"
SA_PASSWORD: $(UMBRACO__CMS__UNATTENDED__UNATTENDEDUSERPASSWORD)
CONNECTIONSTRINGS__UMBRACODBDSN: "Server=(local);Database=Umbraco;User Id=sa;Password=$(SA_PASSWORD);TrustServerCertificate=True"
WindowsPart1Of3:
vmImage: "windows-latest"
testCommand: "npm run testSqlite -- --shard=1/3"
WindowsPart2Of3:
vmImage: "windows-latest"
testCommand: "npm run testSqlite -- --shard=2/3"
WindowsPart3Of3:
vmImage: "windows-latest"
testCommand: "npm run testSqlite -- --shard=3/3"
pool:
vmImage: $(vmImage)
steps:
@@ -635,14 +698,13 @@ stages:
workingDirectory: tests/Umbraco.Tests.AcceptanceTest
# Install Playwright and dependencies
- pwsh: npx playwright install --with-deps
displayName: Install Playwright
- pwsh: npx playwright install chromium
displayName: Install Playwright only with Chromium browser
workingDirectory: tests/Umbraco.Tests.AcceptanceTest
# Test
- pwsh: npm run test --ignore-certificate-errors
- pwsh: $(testCommand)
displayName: Run Playwright tests
continueOnError: true
workingDirectory: tests/Umbraco.Tests.AcceptanceTest
env:
CI: true
@@ -670,18 +732,28 @@ stages:
# Copy artifacts
- pwsh: |
if (Test-Path tests/Umbraco.Tests.AcceptanceTest/results/*) {
Copy-Item tests/Umbraco.Tests.AcceptanceTest/results $(Build.ArtifactStagingDirectory) -Recurse
Copy-Item tests/Umbraco.Tests.AcceptanceTest/results/* $(Build.ArtifactStagingDirectory) -Recurse
}
displayName: Copy Playwright results
condition: succeededOrFailed()
# Publish
# Publish test artifacts
- task: PublishPipelineArtifact@1
displayName: Publish test artifacts
condition: succeededOrFailed()
inputs:
targetPath: $(Build.ArtifactStagingDirectory)
artifact: 'Acceptance Tests - $(Agent.JobName) - Attempt #$(System.JobAttempt)'
artifact: "Acceptance Test Results - $(Agent.JobName) - Attempt #$(System.JobAttempt)"
# Publish test results
- task: PublishTestResults@2
displayName: "Publish test results"
condition: succeededOrFailed()
inputs:
testResultsFormat: 'JUnit'
testResultsFiles: '*.xml'
searchFolder: "tests/Umbraco.Tests.AcceptanceTest/results"
testRunTitle: "$(Agent.JobName)"
###############################################
## Release
@@ -695,6 +767,8 @@ stages:
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
@@ -721,6 +795,8 @@ stages:
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
@@ -40,7 +40,9 @@ public static class UmbracoBuilderAuthExtensions
.SetLogoutEndpointUris(
Paths.MemberApi.LogoutEndpoint.TrimStart(Constants.CharArrays.ForwardSlash))
.SetRevocationEndpointUris(
Paths.MemberApi.RevokeEndpoint.TrimStart(Constants.CharArrays.ForwardSlash));
Paths.MemberApi.RevokeEndpoint.TrimStart(Constants.CharArrays.ForwardSlash))
.SetUserinfoEndpointUris(
Paths.MemberApi.UserinfoEndpoint.TrimStart(Constants.CharArrays.ForwardSlash));
// Enable authorization code flow with PKCE
options
@@ -52,7 +54,8 @@ public static class UmbracoBuilderAuthExtensions
options
.UseAspNetCore()
.EnableAuthorizationEndpointPassthrough()
.EnableLogoutEndpointPassthrough();
.EnableLogoutEndpointPassthrough()
.EnableUserinfoEndpointPassthrough();
// Enable reference tokens
// - see https://documentation.openiddict.com/configuration/token-storage.html
@@ -14,6 +14,8 @@ public static class Paths
public static readonly string RevokeEndpoint = EndpointPath($"{EndpointTemplate}/revoke");
public static readonly string UserinfoEndpoint = EndpointPath($"{EndpointTemplate}/userinfo");
// NOTE: we're NOT using /api/v1.0/ here because it will clash with the Delivery API docs
private static string EndpointPath(string relativePath) => $"/umbraco/delivery/api/v1/{relativePath}";
}
@@ -145,6 +145,11 @@ public class ByRouteContentApiController : ContentApiItemControllerBase
path = DecodePath(path);
path = path.Length == 0 ? "/" : path;
if (_apiContentPathResolver.IsResolvablePath(path) is false)
{
return NotFound();
}
IPublishedContent? contentItem = GetContent(path);
if (contentItem is not null)
{
@@ -15,6 +15,7 @@ namespace Umbraco.Cms.Api.Delivery.Controllers.Content;
[ApiExplorerSettings(GroupName = "Content")]
[LocalizeFromAcceptLanguageHeader]
[ValidateStartItem]
[AddVaryHeader]
[OutputCache(PolicyName = Constants.DeliveryApi.OutputCache.ContentCachePolicy)]
public abstract class ContentApiControllerBase : DeliveryApiControllerBase
{
@@ -0,0 +1,28 @@
using Asp.Versioning;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using OpenIddict.Server.AspNetCore;
using Umbraco.Cms.Api.Delivery.Routing;
using Umbraco.Cms.Core.DeliveryApi;
namespace Umbraco.Cms.Api.Delivery.Controllers.Security;
[ApiVersion("1.0")]
[ApiController]
[VersionedDeliveryApiRoute(Common.Security.Paths.MemberApi.EndpointTemplate)]
[ApiExplorerSettings(IgnoreApi = true)]
[Authorize(AuthenticationSchemes = OpenIddictServerAspNetCoreDefaults.AuthenticationScheme)]
public class CurrentMemberController : DeliveryApiControllerBase
{
private readonly ICurrentMemberClaimsProvider _currentMemberClaimsProvider;
public CurrentMemberController(ICurrentMemberClaimsProvider currentMemberClaimsProvider)
=> _currentMemberClaimsProvider = currentMemberClaimsProvider;
[HttpGet("userinfo")]
public async Task<IActionResult> Userinfo()
{
Dictionary<string, object> claims = await _currentMemberClaimsProvider.GetClaimsAsync();
return Ok(claims);
}
}
@@ -60,6 +60,7 @@ public static class UmbracoBuilderExtensions
builder.Services.AddSingleton<IApiMediaQueryService, ApiMediaQueryService>();
builder.Services.AddTransient<IMemberApplicationManager, MemberApplicationManager>();
builder.Services.AddTransient<IRequestMemberAccessService, RequestMemberAccessService>();
builder.Services.AddTransient<ICurrentMemberClaimsProvider, CurrentMemberClaimsProvider>();
builder.Services.ConfigureOptions<ConfigureUmbracoDeliveryApiSwaggerGenOptions>();
builder.AddUmbracoApiOpenApiUI();
@@ -0,0 +1,13 @@
using Microsoft.AspNetCore.Mvc.Filters;
namespace Umbraco.Cms.Api.Delivery.Filters;
public sealed class AddVaryHeaderAttribute : ActionFilterAttribute
{
private const string Vary = "Accept-Language, Preview, Start-Item";
public override void OnResultExecuting(ResultExecutingContext context)
=> context.HttpContext.Response.Headers.Vary = context.HttpContext.Response.Headers.Vary.Count > 0
? $"{context.HttpContext.Response.Headers.Vary}, {Vary}"
: Vary;
}
@@ -1,6 +1,5 @@
using Umbraco.Cms.Api.Delivery.Indexing.Filters;
using Umbraco.Cms.Core.DeliveryApi;
using Umbraco.Extensions;
namespace Umbraco.Cms.Api.Delivery.Querying.Filters;
@@ -15,15 +14,15 @@ public sealed class ContentTypeFilter : IFilterHandler
/// <inheritdoc/>
public FilterOption BuildFilterOption(string filter)
{
var alias = filter.Substring(ContentTypeSpecifier.Length);
var filterValue = filter.Substring(ContentTypeSpecifier.Length);
var negate = filterValue.StartsWith('!');
var aliases = filterValue.TrimStart('!').Split(',', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries);
return new FilterOption
{
FieldName = ContentTypeFilterIndexer.FieldName,
Values = alias.IsNullOrWhiteSpace() == false
? new[] { alias.TrimStart('!') }
: Array.Empty<string>(),
Operator = alias.StartsWith('!')
Values = aliases,
Operator = negate
? FilterOperation.IsNot
: FilterOperation.Is
};
@@ -1,5 +1,7 @@
using Microsoft.Extensions.DependencyInjection;
using Umbraco.Cms.Api.Delivery.Indexing.Selectors;
using Umbraco.Cms.Core.DeliveryApi;
using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Core.Models.PublishedContent;
using Umbraco.Cms.Core.PublishedCache;
using Umbraco.Extensions;
@@ -10,10 +12,21 @@ public sealed class AncestorsSelector : QueryOptionBase, ISelectorHandler
{
private const string AncestorsSpecifier = "ancestors:";
private readonly IPublishedSnapshotAccessor _publishedSnapshotAccessor;
private readonly IRequestPreviewService _requestPreviewService;
public AncestorsSelector(IPublishedSnapshotAccessor publishedSnapshotAccessor, IRequestRoutingService requestRoutingService)
: base(publishedSnapshotAccessor, requestRoutingService) =>
[Obsolete("Please use the non-obsolete constructor. Will be removed in V17.")]
public AncestorsSelector(IPublishedSnapshotAccessor publishedSnapshotAccessor,
IRequestRoutingService requestRoutingService)
: this(publishedSnapshotAccessor, requestRoutingService, StaticServiceProvider.Instance.GetRequiredService<IRequestPreviewService>())
{
}
public AncestorsSelector(IPublishedSnapshotAccessor publishedSnapshotAccessor, IRequestRoutingService requestRoutingService, IRequestPreviewService requestPreviewService)
: base(publishedSnapshotAccessor, requestRoutingService)
{
_publishedSnapshotAccessor = publishedSnapshotAccessor;
_requestPreviewService = requestPreviewService;
}
/// <inheritdoc />
public bool CanHandle(string query)
@@ -37,10 +50,20 @@ public sealed class AncestorsSelector : QueryOptionBase, ISelectorHandler
};
}
IPublishedSnapshot publishedSnapshot = _publishedSnapshotAccessor.GetRequiredPublishedSnapshot();
IPublishedContentCache contentCache = _publishedSnapshotAccessor.GetRequiredPublishedSnapshot()?.Content
?? throw new InvalidOperationException("Could not obtain the content cache");
IPublishedContent contentItem = publishedSnapshot.Content?.GetById((Guid)id)
?? throw new InvalidOperationException("Could not obtain the content cache");
IPublishedContent? contentItem = contentCache.GetById(_requestPreviewService.IsPreview(), id.Value);
if (contentItem is null)
{
// no such content item, make sure the selector does not yield any results
return new SelectorOption
{
FieldName = AncestorsSelectorIndexer.FieldName,
Values = Array.Empty<string>()
};
}
var ancestorKeys = contentItem.Ancestors().Select(a => a.Key.ToString("D")).ToArray();
@@ -0,0 +1,44 @@
using OpenIddict.Abstractions;
using Umbraco.Cms.Core.DeliveryApi;
using Umbraco.Cms.Core.Security;
namespace Umbraco.Cms.Api.Delivery.Services;
// NOTE: this is public and unsealed to allow overriding the default claims with minimal effort.
public class CurrentMemberClaimsProvider : ICurrentMemberClaimsProvider
{
private readonly IMemberManager _memberManager;
public CurrentMemberClaimsProvider(IMemberManager memberManager)
=> _memberManager = memberManager;
public virtual async Task<Dictionary<string, object>> GetClaimsAsync()
{
MemberIdentityUser? memberIdentityUser = await _memberManager.GetCurrentMemberAsync();
return memberIdentityUser is not null
? await GetClaimsForMemberIdentityAsync(memberIdentityUser)
: throw new InvalidOperationException("Could not retrieve the current member. This method should only ever be invoked when a member has been authorized.");
}
protected virtual async Task<Dictionary<string, object>> GetClaimsForMemberIdentityAsync(MemberIdentityUser memberIdentityUser)
{
var claims = new Dictionary<string, object>
{
[OpenIddictConstants.Claims.Subject] = memberIdentityUser.Key
};
if (memberIdentityUser.Name is not null)
{
claims[OpenIddictConstants.Claims.Name] = memberIdentityUser.Name;
}
if (memberIdentityUser.Email is not null)
{
claims[OpenIddictConstants.Claims.Email] = memberIdentityUser.Email;
}
claims[OpenIddictConstants.Claims.Role] = await _memberManager.GetRolesAsync(memberIdentityUser);
return claims;
}
}
@@ -1,4 +1,4 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http;
using Umbraco.Cms.Core.DeliveryApi;
namespace Umbraco.Cms.Api.Delivery.Services;
@@ -11,5 +11,5 @@ internal sealed class RequestPreviewService : RequestHeaderHandler, IRequestPrev
}
/// <inheritdoc />
public bool IsPreview() => GetHeaderValue("Preview") == "true";
public bool IsPreview() => string.Equals(GetHeaderValue("Preview"), "true", StringComparison.OrdinalIgnoreCase);
}
@@ -42,31 +42,42 @@ internal sealed class RequestRedirectService : RoutingServiceBase, IRequestRedir
{
requestedPath = requestedPath.EnsureStartsWith("/");
IPublishedContent? startItem = GetStartItem();
// must append the root content url segment if it is not hidden by config, because
// the URL tracking is based on the actual URL, including the root content url segment
if (_globalSettings.HideTopLevelNodeFromPath == false)
if (_globalSettings.HideTopLevelNodeFromPath == false && startItem?.UrlSegment != null)
{
IPublishedContent? startItem = GetStartItem();
if (startItem?.UrlSegment != null)
{
requestedPath = $"{startItem.UrlSegment.EnsureStartsWith("/")}{requestedPath}";
}
requestedPath = $"{startItem.UrlSegment.EnsureStartsWith("/")}{requestedPath}";
}
var culture = _requestCultureService.GetRequestedCulture();
// append the configured domain content ID to the path if we have a domain bound request,
// because URL tracking registers the tracked url like "{domain content ID}/{content path}"
Uri contentRoute = GetDefaultRequestUri(requestedPath);
DomainAndUri? domainAndUri = GetDomainAndUriForRoute(contentRoute);
if (domainAndUri != null)
// important: redirect URLs are always tracked without trailing slashes
requestedPath = requestedPath.TrimEnd("/");
IRedirectUrl? redirectUrl = _redirectUrlService.GetMostRecentRedirectUrl(requestedPath, culture);
// if a redirect URL was not found, try by appending the start item ID because URL tracking might have tracked
// a redirect with "{root content ID}/{content path}"
if (redirectUrl is null && startItem is not null)
{
requestedPath = GetContentRoute(domainAndUri, contentRoute);
culture ??= domainAndUri.Culture;
redirectUrl = _redirectUrlService.GetMostRecentRedirectUrl($"{startItem.Id}{requestedPath}", culture);
}
// still no redirect URL found - try looking for a configured domain if we have a domain bound request,
// because URL tracking might have tracked a redirect with "{domain content ID}/{content path}"
if (redirectUrl is null)
{
Uri contentRoute = GetDefaultRequestUri(requestedPath);
DomainAndUri? domainAndUri = GetDomainAndUriForRoute(contentRoute);
if (domainAndUri is not null)
{
requestedPath = GetContentRoute(domainAndUri, contentRoute);
culture ??= domainAndUri.Culture;
redirectUrl = _redirectUrlService.GetMostRecentRedirectUrl(requestedPath, culture);
}
}
// important: redirect URLs are always tracked without trailing slashes
IRedirectUrl? redirectUrl = _redirectUrlService.GetMostRecentRedirectUrl(requestedPath.TrimEnd("/"), culture);
IPublishedContent? content = redirectUrl != null
? _apiPublishedContentCache.GetById(redirectUrl.ContentKey)
: null;
@@ -36,7 +36,7 @@ internal abstract class RoutingServiceBase
}
protected static string GetContentRoute(DomainAndUri domainAndUri, Uri contentRoute)
=> $"{domainAndUri.ContentId}{DomainUtilities.PathRelativeToDomain(domainAndUri.Uri, contentRoute.AbsolutePath)}";
=> $"{domainAndUri.ContentId}{DomainUtilities.PathRelativeToDomain(domainAndUri.Uri, contentRoute.LocalPath)}"; // Use LocalPath over AbsolutePath to keep the path decoded.
protected DomainAndUri? GetDomainAndUriForRoute(Uri contentUrl)
{
@@ -13,6 +13,9 @@
<AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleTo">
<_Parameter1>Umbraco.Tests.UnitTests</_Parameter1>
</AssemblyAttribute>
<AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleTo">
<_Parameter1>Umbraco.Tests.Integration</_Parameter1>
</AssemblyAttribute>
<AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleTo">
<_Parameter1>DynamicProxyGenAssembly2</_Parameter1>
</AssemblyAttribute>
@@ -1,3 +1,4 @@
using System.Globalization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Headers;
using Microsoft.Extensions.Options;
@@ -48,16 +49,26 @@ public sealed class ConfigureImageSharpMiddlewareOptions : IConfigureOptions<Ima
return Task.CompletedTask;
}
int width = context.Parser.ParseValue<int>(context.Commands.GetValueOrDefault(ResizeWebProcessor.Width), context.Culture);
if (width <= 0 || width > _imagingSettings.Resize.MaxWidth)
if (context.Commands.Contains(ResizeWebProcessor.Width))
{
context.Commands.Remove(ResizeWebProcessor.Width);
if (!int.TryParse(context.Commands.GetValueOrDefault(ResizeWebProcessor.Width), NumberStyles.Integer,
CultureInfo.InvariantCulture, out var width)
|| width < 0
|| width >= _imagingSettings.Resize.MaxWidth)
{
context.Commands.Remove(ResizeWebProcessor.Width);
}
}
int height = context.Parser.ParseValue<int>(context.Commands.GetValueOrDefault(ResizeWebProcessor.Height), context.Culture);
if (height <= 0 || height > _imagingSettings.Resize.MaxHeight)
if (context.Commands.Contains(ResizeWebProcessor.Height))
{
context.Commands.Remove(ResizeWebProcessor.Height);
if (!int.TryParse(context.Commands.GetValueOrDefault(ResizeWebProcessor.Height), NumberStyles.Integer,
CultureInfo.InvariantCulture, out var height)
|| height < 0
|| height >= _imagingSettings.Resize.MaxHeight)
{
context.Commands.Remove(ResizeWebProcessor.Height);
}
}
return Task.CompletedTask;
@@ -5,7 +5,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="SixLabors.ImageSharp" VersionOverride="[2.1.9, 3)" />
<PackageReference Include="SixLabors.ImageSharp" VersionOverride="[2.1.10, 3)" />
<PackageReference Include="SixLabors.ImageSharp.Web" VersionOverride="[2.0.2, 3)" />
</ItemGroup>
@@ -7,6 +7,11 @@
<ItemGroup>
<!-- Take top-level depedendency on Azure.Identity, because Microsoft.EntityFrameworkCore.SqlServer depends on a vulnerable version -->
<PackageReference Include="Azure.Identity" />
<!-- Both Azure.Identity, Microsoft.EntityFrameworkCore.SqlServer,NPoco.SqlServer, and more bring in a vulnerable version of System.Text.Json -->
<PackageReference Include="System.Text.Json" />
<!-- Both Microsoft.EntityFrameworkCore.SqlServer and NPoco.SqlServer bring in a vulnerable version of Microsoft.Data.SqlClient -->
<PackageReference Include="Microsoft.Data.SqlClient" />
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" />
<!-- Both OpenIddict.AspNetCore, Npoco.SqlServer and Microsoft.EntityFrameworkCore.SqlServer bring in a vulnerable version of Microsoft.IdentityModel.JsonWebTokens -->
@@ -170,7 +170,9 @@ internal class SqlServerEFCoreDistributedLockingMechanism<T> : IDistributedLocki
"A transaction with minimum ReadCommitted isolation level is required.");
}
#pragma warning disable EF1002
var rowsAffected = await dbContext.Database.ExecuteSqlRawAsync(@$"SET LOCK_TIMEOUT {(int)_timeout.TotalMilliseconds};UPDATE umbracoLock WITH (REPEATABLEREAD) SET value = (CASE WHEN (value=1) THEN -1 ELSE 1 END) WHERE id={LockId}");
#pragma warning restore EF1002
if (rowsAffected == 0)
{
@@ -10,6 +10,10 @@
<!-- Take top-level depedendency on Microsoft.Extensions.Caching.Memory, because Microsoft.EntityFrameworkCore.* depends on a vulnerable version -->
<PackageReference Include="Microsoft.Extensions.Caching.Memory" />
<!-- Both Azure.Identity, Microsoft.EntityFrameworkCore.SqlServer,NPoco.SqlServer, and more bring in a vulnerable version of System.Text.Json -->
<PackageReference Include="System.Text.Json" />
<!-- Both Microsoft.EntityFrameworkCore.SqlServer and NPoco.SqlServer bring in a vulnerable version of Microsoft.Data.SqlClient -->
<PackageReference Include="Microsoft.Data.SqlClient" />
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" />
@@ -7,6 +7,11 @@
<ItemGroup>
<!-- Take top-level depedendency on Azure.Identity, because NPoco.SqlServer depends on a vulnerable version -->
<PackageReference Include="Azure.Identity" />
<!-- Both Azure.Identity, Microsoft.EntityFrameworkCore.SqlServer,NPoco.SqlServer, and more bring in a vulnerable version of System.Text.Json -->
<PackageReference Include="System.Text.Json" />
<!-- Both Microsoft.EntityFrameworkCore.SqlServer and NPoco.SqlServer bring in a vulnerable version of Microsoft.Data.SqlClient -->
<PackageReference Include="Microsoft.Data.SqlClient" />
<PackageReference Include="NPoco.SqlServer" />
<!-- Both OpenIddict.AspNetCore, Npoco.SqlServer and Microsoft.EntityFrameworkCore.SqlServer bring in a vulnerable version of Microsoft.IdentityModel.JsonWebTokens -->
@@ -30,13 +30,13 @@
</Target>
<Target Name="BuildBelle">
<Exec WorkingDirectory="$(ProjectDir)..\Umbraco.Web.UI.Client\" Command="npm ci --no-fund --no-audit --prefer-offline" />
<Exec WorkingDirectory="$(ProjectDir)..\Umbraco.Web.UI.Client\" Command="npm run build:skip-tests" />
<Exec WorkingDirectory="$(ProjectDir)..\Umbraco.Web.UI.Client\" Command="npm ci --no-fund --no-audit --prefer-offline" Timeout="600000" />
<Exec WorkingDirectory="$(ProjectDir)..\Umbraco.Web.UI.Client\" Command="npm run build:skip-tests" Timeout="600000" />
</Target>
<Target Name="BuildLogin">
<Exec WorkingDirectory="$(ProjectDir)..\Umbraco.Web.UI.Login\" Command="npm ci --no-fund --no-audit --prefer-offline" />
<Exec WorkingDirectory="$(ProjectDir)..\Umbraco.Web.UI.Login\" Command="npm run build" />
<Exec WorkingDirectory="$(ProjectDir)..\Umbraco.Web.UI.Login\" Command="npm ci --no-fund --no-audit --prefer-offline" Timeout="600000" />
<Exec WorkingDirectory="$(ProjectDir)..\Umbraco.Web.UI.Login\" Command="npm run build" Timeout="600000" />
</Target>
<Target Name="CleanStaticAssetsPreconditions" AfterTargets="Clean" Condition="'$(UmbracoBuild)' == ''">
@@ -40,7 +40,7 @@
<!-- Generate JSON schema on build (and before copying to project) -->
<Target Name="GenerateAppsettingsSchema" BeforeTargets="Build;CopyUmbracoJsonSchemaFiles" Condition="!Exists('$(_UmbracoCmsJsonSchemaReference)')">
<Message Text="Generating $(_UmbracoCmsJsonSchemaReference) because it doesn't exist" Importance="high" />
<Exec WorkingDirectory="$(MSBuildThisFileDirectory)..\..\tools\Umbraco.JsonSchema" Command="dotnet run --configuration $(Configuration) -- --outputFile &quot;$(MSBuildThisFileDirectory)$(_UmbracoCmsJsonSchemaReference)&quot;" />
<Exec WorkingDirectory="$(MSBuildThisFileDirectory)..\..\tools\Umbraco.JsonSchema" Command="dotnet run --configuration $(Configuration) -- --outputFile &quot;$(MSBuildThisFileDirectory)$(_UmbracoCmsJsonSchemaReference)&quot;" Timeout="600000" />
</Target>
<!-- Remove generated JSON schema on clean -->
+4 -2
View File
@@ -43,7 +43,7 @@ public static class AppCacheExtensions
public static T? GetCacheItem<T>(this IAppCache provider, string cacheKey)
{
var result = provider.Get(cacheKey);
if (result == null)
if (IsRetrievedItemNull(result))
{
return default;
}
@@ -54,11 +54,13 @@ public static class AppCacheExtensions
public static T? GetCacheItem<T>(this IAppCache provider, string cacheKey, Func<T> getCacheItem)
{
var result = provider.Get(cacheKey, () => getCacheItem());
if (result == null)
if (IsRetrievedItemNull(result))
{
return default;
}
return result.TryConvertTo<T>().Result;
}
private static bool IsRetrievedItemNull(object? result) => result is null or (object)Cms.Core.Constants.Cache.NullRepresentationInCache;
}
+2
View File
@@ -22,4 +22,6 @@ public static class CacheKeys
public const string ContentRecycleBinCacheKey = "recycleBin_content";
public const string MediaRecycleBinCacheKey = "recycleBin_media";
public const string MemberUserNameCachePrefix = "uRepo_userNameKey+";
}
@@ -142,7 +142,9 @@ public static class DistributedCacheExtensions
Id = x.Item.Id,
Key = x.Item.Key,
ChangeTypes = x.ChangeTypes,
Blueprint = x.Item.Blueprint
Blueprint = x.Item.Blueprint,
PublishedCultures = x.PublishedCultures?.ToArray(),
UnpublishedCultures = x.UnpublishedCultures?.ToArray()
});
dc.RefreshByPayload(ContentCacheRefresher.UniqueId, payloads);
@@ -0,0 +1,16 @@
namespace Umbraco.Cms.Core.Cache.PartialViewCacheInvalidators;
/// <summary>
/// Defines behaviours for clearing of cached partials views that are configured to be cached individually by member.
/// </summary>
public interface IMemberPartialViewCacheInvalidator
{
/// <summary>
/// Clears the partial view cache items for the specified member ids.
/// </summary>
/// <param name="memberIds">The member Ids to clear the cache for.</param>
/// <remarks>
/// Called from the <see cref="MemberCacheRefresher"/> when a member is saved or deleted.
/// </remarks>
void ClearPartialViewCacheItems(IEnumerable<int> memberIds);
}
@@ -182,6 +182,10 @@ public sealed class ContentCacheRefresher : PayloadCacheRefresherBase<ContentCac
public TreeChangeTypes ChangeTypes { get; init; }
public bool Blueprint { get; init; }
public string[]? PublishedCultures { get; init; }
public string[]? UnpublishedCultures { get; init; }
}
#endregion
@@ -1,5 +1,8 @@
// using Newtonsoft.Json;
using Microsoft.Extensions.DependencyInjection;
using Umbraco.Cms.Core.Cache.PartialViewCacheInvalidators;
using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Core.Events;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Notifications;
@@ -15,10 +18,37 @@ public sealed class MemberCacheRefresher : PayloadCacheRefresherBase<MemberCache
public static readonly Guid UniqueId = Guid.Parse("E285DF34-ACDC-4226-AE32-C0CB5CF388DA");
private readonly IIdKeyMap _idKeyMap;
private readonly IMemberPartialViewCacheInvalidator _memberPartialViewCacheInvalidator;
public MemberCacheRefresher(AppCaches appCaches, IJsonSerializer serializer, IIdKeyMap idKeyMap, IEventAggregator eventAggregator, ICacheRefresherNotificationFactory factory)
: base(appCaches, serializer, eventAggregator, factory) =>
[Obsolete("Use the non obsoleted constructor instead. Scheduled for removal in v17")]
public MemberCacheRefresher(
AppCaches appCaches,
IJsonSerializer serializer,
IIdKeyMap idKeyMap,
IEventAggregator eventAggregator,
ICacheRefresherNotificationFactory factory)
: this(
appCaches,
serializer,
idKeyMap,
eventAggregator,
factory,
StaticServiceProvider.Instance.GetRequiredService<IMemberPartialViewCacheInvalidator>())
{
}
public MemberCacheRefresher(
AppCaches appCaches,
IJsonSerializer serializer,
IIdKeyMap idKeyMap,
IEventAggregator eventAggregator,
ICacheRefresherNotificationFactory factory,
IMemberPartialViewCacheInvalidator memberPartialViewCacheInvalidator)
: base(appCaches, serializer, eventAggregator, factory)
{
_idKeyMap = idKeyMap;
_memberPartialViewCacheInvalidator = memberPartialViewCacheInvalidator;
}
#region Indirect
@@ -67,17 +97,30 @@ public sealed class MemberCacheRefresher : PayloadCacheRefresherBase<MemberCache
private void ClearCache(params JsonPayload[] payloads)
{
AppCaches.ClearPartialViewCache();
// Clear the partial views cache for all partials that are cached by member, for the updates members.
_memberPartialViewCacheInvalidator.ClearPartialViewCacheItems(payloads.Select(p => p.Id));
Attempt<IAppPolicyCache?> memberCache = AppCaches.IsolatedCaches.Get<IMember>();
foreach (JsonPayload p in payloads)
{
_idKeyMap.ClearCache(p.Id);
if (memberCache.Success)
if (memberCache.Success is false)
{
memberCache.Result?.Clear(RepositoryCacheKeys.GetKey<IMember, int>(p.Id));
memberCache.Result?.Clear(RepositoryCacheKeys.GetKey<IMember, string>(p.Username));
continue;
}
memberCache.Result?.Clear(RepositoryCacheKeys.GetKey<IMember, int>(p.Id));
memberCache.Result?.Clear(RepositoryCacheKeys.GetKey<IMember, string>(p.Username));
// This specific cache key was introduced to fix an issue where the member username could not be the same as the member id, because the cache keys collided.
// This is done in a bit of a hacky way, because the cache key is created internally in the repository, but we need to clear it here.
// Ideally, we want to use a shared way of generating the key between this and the repository.
// Additionally, the RepositoryCacheKeys actually caches the string to avoid re-allocating memory; we would like to also use this in the repository
// See:
// https://github.com/umbraco/Umbraco-CMS/pull/17350
// https://github.com/umbraco/Umbraco-CMS/pull/17815
memberCache.Result?.Clear(RepositoryCacheKeys.GetKey<IMember, string>(CacheKeys.MemberUserNameCachePrefix + p.Username));
}
}
}
@@ -11,6 +11,7 @@ public class RepositoryCachePolicyOptions
public RepositoryCachePolicyOptions(Func<int> performCount)
{
PerformCount = performCount;
CacheNullValues = false;
GetAllCacheValidateCount = true;
GetAllCacheAllowZeroCount = false;
}
@@ -21,6 +22,7 @@ public class RepositoryCachePolicyOptions
public RepositoryCachePolicyOptions()
{
PerformCount = null;
CacheNullValues = false;
GetAllCacheValidateCount = false;
GetAllCacheAllowZeroCount = false;
}
@@ -30,6 +32,11 @@ public class RepositoryCachePolicyOptions
/// </summary>
public Func<int>? PerformCount { get; set; }
/// <summary>
/// True if the Get method will cache null results so that the db is not hit for repeated lookups
/// </summary>
public bool CacheNullValues { get; set; }
/// <summary>
/// True/false as to validate the total item count when all items are returned from cache, the default is true but this
/// means that a db lookup will occur - though that lookup will probably be significantly less expensive than the
+1 -1
View File
@@ -34,7 +34,7 @@ public class TypeFinder : ITypeFinder
"ServiceStack.", "SqlCE4Umbraco,", "Superpower,", // used by Serilog
"System.", "TidyNet,", "TidyNet.", "WebDriver,", "itextsharp,", "mscorlib,", "NUnit,", "NUnit.", "NUnit3.",
"Selenium.", "ImageProcessor", "MiniProfiler.", "Owin,", "SQLite",
"ReSharperTestRunner", "ReSharperTestRunner32", "ReSharperTestRunner64", // These are used by the Jetbrains Rider IDE and Visual Studio ReSharper Extension
"ReSharperTestRunner", "ReSharperTestRunner32", "ReSharperTestRunner64", "ReSharperTestRunnerArm32", "ReSharperTestRunnerArm64", // These are used by the Jetbrains Rider IDE and Visual Studio ReSharper Extension
};
private static readonly ConcurrentDictionary<string, Type?> TypeNamesCache = new();
@@ -160,6 +160,7 @@ public class ContentSettings
internal const bool StaticDisableUnpublishWhenReferenced = false;
internal const bool StaticAllowEditInvariantFromNonDefault = false;
internal const bool StaticShowDomainWarnings = true;
internal const bool StaticShowUnroutableContentWarnings = true;
/// <summary>
/// Gets or sets a value for the content notification settings.
@@ -285,4 +286,10 @@ public class ContentSettings
/// </summary>
[DefaultValue(StaticShowDomainWarnings)]
public bool ShowDomainWarnings { get; set; } = StaticShowDomainWarnings;
/// <summary>
/// Gets or sets a value indicating whether to show unroutable content warnings.
/// </summary>
[DefaultValue(StaticShowUnroutableContentWarnings)]
public bool ShowUnroutableContentWarnings { get; set; } = StaticShowUnroutableContentWarnings;
}
@@ -129,6 +129,7 @@ public class GlobalSettings
/// <summary>
/// Gets or sets a value indicating whether to install the database when it is missing.
/// </summary>
[Obsolete("This option will be removed in V16.")]
[DefaultValue(StaticInstallMissingDatabase)]
public bool InstallMissingDatabase { get; set; } = StaticInstallMissingDatabase;
@@ -16,6 +16,7 @@ public class ModelsBuilderSettings
internal const string StaticModelsDirectory = "~/umbraco/models";
internal const bool StaticAcceptUnsafeModelsDirectory = false;
internal const int StaticDebugLevel = 0;
internal const bool StaticIncludeVersionNumberInGeneratedModels = true;
private bool _flagOutOfDateModels = true;
/// <summary>
@@ -78,4 +79,16 @@ public class ModelsBuilderSettings
/// <remarks>0 means minimal (safe on live site), anything else means more and more details (maybe not safe).</remarks>
[DefaultValue(StaticDebugLevel)]
public int DebugLevel { get; set; } = StaticDebugLevel;
/// <summary>
/// Gets or sets a value indicating whether the version number should be included in generated models.
/// </summary>
/// <remarks>
/// By default this is written to the <see cref="System.CodeDom.Compiler.GeneratedCodeAttribute"/> output in
/// generated code for each property of the model. This can be useful for debugging purposes but isn't essential,
/// and it has the causes the generated code to change every time Umbraco is upgraded. In turn, this leads
/// to unnecessary code file changes that need to be checked into source control. Default is <c>true</c>.
/// </remarks>
[DefaultValue(StaticIncludeVersionNumberInGeneratedModels)]
public bool IncludeVersionNumberInGeneratedModels { get; set; } = StaticIncludeVersionNumberInGeneratedModels;
}
@@ -15,6 +15,7 @@ public class RequestHandlerSettings
{
internal const bool StaticAddTrailingSlash = true;
internal const string StaticConvertUrlsToAscii = "try";
internal const string StaticConvertFileNamesToAscii = "false";
internal const bool StaticEnableDefaultCharReplacements = true;
internal static readonly CharItem[] DefaultCharCollection =
@@ -73,6 +74,22 @@ public class RequestHandlerSettings
/// </summary>
public bool ShouldTryConvertUrlsToAscii => ConvertUrlsToAscii.InvariantEquals("try");
/// <summary>
/// Gets or sets a value indicating whether to convert file names to ASCII (valid values: "true", "try" or "false").
/// </summary>
[DefaultValue(StaticConvertFileNamesToAscii)]
public string ConvertFileNamesToAscii { get; set; } = StaticConvertFileNamesToAscii;
/// <summary>
/// Gets a value indicating whether URLs should be converted to ASCII.
/// </summary>
public bool ShouldConvertFileNamesToAscii => ConvertFileNamesToAscii.InvariantEquals("true");
/// <summary>
/// Gets a value indicating whether URLs should be tried to be converted to ASCII.
/// </summary>
public bool ShouldTryConvertFileNamesToAscii => ConvertFileNamesToAscii.InvariantEquals("try");
/// <summary>
/// Disable all default character replacements
/// </summary>
@@ -8,7 +8,7 @@ namespace Umbraco.Cms.Core.Configuration.Models;
public class RichTextEditorSettings
{
internal const string StaticValidElements =
"+a[id|style|rel|data-id|data-udi|rev|charset|hreflang|dir|lang|tabindex|accesskey|type|name|href|target|title|class|onfocus|onblur|onclick|ondblclick|onmousedown|onmouseup|onmouseover|onmousemove|onmouseout|onkeypress|onkeydown|onkeyup],-strong/-b[class|style],-em/-i[class|style],-strike[class|style],-s[class|style],-u[class|style],#p[id|style|dir|class|align],-ol[class|reversed|start|style|type],-ul[class|style],-li[class|style],br[class],img[id|dir|lang|longdesc|usemap|style|class|src|onmouseover|onmouseout|border|alt=|title|hspace|vspace|width|height|align|umbracoorgwidth|umbracoorgheight|onresize|onresizestart|onresizeend|rel|data-id],-sub[style|class],-sup[style|class],-blockquote[dir|style|class],-table[border=0|cellspacing|cellpadding|width|height|class|align|summary|style|dir|id|lang|bgcolor|background|bordercolor],-tr[id|lang|dir|class|rowspan|width|height|align|valign|style|bgcolor|background|bordercolor],tbody[id|class],thead[id|class],tfoot[id|class],#td[id|lang|dir|class|colspan|rowspan|width|height|align|valign|style|bgcolor|background|bordercolor|scope],-th[id|lang|dir|class|colspan|rowspan|width|height|align|valign|style|scope],caption[id|lang|dir|class|style],-div[id|dir|class|align|style],-span[class|align|style],-pre[class|align|style],address[class|align|style],-h1[id|dir|class|align|style],-h2[id|dir|class|align|style],-h3[id|dir|class|align|style],-h4[id|dir|class|align|style],-h5[id|dir|class|align|style],-h6[id|style|dir|class|align|style],hr[class|style],small[class|style],dd[id|class|title|style|dir|lang],dl[id|class|title|style|dir|lang],dt[id|class|title|style|dir|lang],object[class|id|width|height|codebase|*],param[name|value|_value|class],embed[type|width|height|src|class|*],map[name|class],area[shape|coords|href|alt|target|class],bdo[class],button[class],iframe[*],figure,figcaption,cite,video[*],audio[*],picture[*],source[*],canvas[*]";
"+a[id|style|rel|data-id|data-udi|rev|charset|hreflang|dir|lang|tabindex|accesskey|type|name|href|target|title|class|onfocus|onblur|onclick|ondblclick|onmousedown|onmouseup|onmouseover|onmousemove|onmouseout|onkeypress|onkeydown|onkeyup],-strong/-b[class|style],-em/-i[class|style],-strike[class|style],-s[class|style],-u[class|style],#p[id|style|dir|class|align],-ol[class|reversed|start|style|type],-ul[class|style],-li[class|style],br[class],img[id|dir|lang|longdesc|usemap|style|class|src|onmouseover|onmouseout|border|alt=|title|hspace|vspace|width|height|align|umbracoorgwidth|umbracoorgheight|onresize|onresizestart|onresizeend|rel|data-id],-sub[style|class],-sup[style|class],-blockquote[dir|style|class],-table[border=0|cellspacing|cellpadding|width|height|class|align|summary|style|dir|id|lang|bgcolor|background|bordercolor],-tr[id|lang|dir|class|rowspan|width|height|align|valign|style|bgcolor|background|bordercolor],tbody[id|class],thead[id|class],tfoot[id|class],#td[id|lang|dir|class|colspan|rowspan|width|height|align|valign|style|bgcolor|background|bordercolor|scope],-th[id|lang|dir|class|colspan|rowspan|width|height|align|valign|style|scope],caption[id|lang|dir|class|style],-div[id|dir|class|align|style],-span[class|align|style],-pre[class|align|style],address[class|align|style],-h1[id|dir|class|align|style],-h2[id|dir|class|align|style],-h3[id|dir|class|align|style],-h4[id|dir|class|align|style],-h5[id|dir|class|align|style],-h6[id|style|dir|class|align|style],hr[class|style],small[class|style],dd[id|class|title|style|dir|lang],dl[id|class|title|style|dir|lang],dt[id|class|title|style|dir|lang],object[class|id|width|height|codebase|*],param[name|value|_value|class],embed[type|width|height|src|class|*],map[name|class],area[shape|coords|href|alt|target|class],bdo[class],button[class],iframe[*],figure,figcaption,cite,video[*],audio[*],picture[*],source[*],canvas[*],code";
internal const string StaticInvalidElements = "font";
@@ -2,6 +2,7 @@
// See LICENSE for more details.
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
namespace Umbraco.Cms.Core.Configuration.Models;
@@ -19,12 +20,16 @@ public class SecuritySettings
internal const bool StaticAllowEditInvariantFromNonDefault = false;
internal const bool StaticAllowConcurrentLogins = false;
internal const string StaticAuthCookieName = "UMB_UCONTEXT";
internal const bool StaticUsernameIsEmail = true;
internal const bool StaticMemberRequireUniqueEmail = true;
internal const string StaticAllowedUserNameCharacters =
"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._@+\\";
internal const int StaticMemberDefaultLockoutTimeInMinutes = 30 * 24 * 60;
internal const int StaticUserDefaultLockoutTimeInMinutes = 30 * 24 * 60;
internal const long StaticUserDefaultFailedLoginDurationInMilliseconds = 1000;
internal const long StaticUserMinimumFailedLoginDurationInMilliseconds = 250;
/// <summary>
/// Gets or sets a value indicating whether to keep the user logged in.
@@ -58,7 +63,14 @@ public class SecuritySettings
/// <summary>
/// Gets or sets a value indicating whether the user's email address is to be considered as their username.
/// </summary>
public bool UsernameIsEmail { get; set; } = true;
[DefaultValue(StaticUsernameIsEmail)]
public bool UsernameIsEmail { get; set; } = StaticUsernameIsEmail;
/// <summary>
/// Gets or sets a value indicating whether the member's email address must be unique.
/// </summary>
[DefaultValue(StaticMemberRequireUniqueEmail)]
public bool MemberRequireUniqueEmail { get; set; } = StaticMemberRequireUniqueEmail;
/// <summary>
/// Gets or sets the set of allowed characters for a username
@@ -116,4 +128,28 @@ public class SecuritySettings
/// </summary>
[DefaultValue(StaticAllowConcurrentLogins)]
public bool AllowConcurrentLogins { get; set; } = StaticAllowConcurrentLogins;
/// <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>
[Range(0, int.MaxValue)] // TODO (V17): Change property type to short and update maximum range to short.MaxValue
[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>
[Range(0, int.MaxValue)] // TODO (V17): Change property type to short and update maximum range to short.MaxValue
[DefaultValue(StaticUserMinimumFailedLoginDurationInMilliseconds)]
public long UserMinimumFailedLoginDurationInMilliseconds { get; set; } = StaticUserMinimumFailedLoginDurationInMilliseconds;
}
+15
View File
@@ -0,0 +1,15 @@
namespace Umbraco.Cms.Core;
public static partial class Constants
{
public static class Cache
{
/// <summary>
/// Defines the string used to represent a null value in the cache.
/// </summary>
/// <remarks>
/// Used in conjunction with the option to cache null values on the repository caches, so we
/// can distinguish a true null "not found" value and a cached null value.</remarks>
public const string NullRepresentationInCache = "*NULL*";
}
}
@@ -15,6 +15,27 @@ public class ApiContentPathResolver : IApiContentPathResolver
_apiPublishedContentCache = apiPublishedContentCache;
}
[Obsolete("No longer used in V15. Scheduled for removal in V15.")]
public virtual bool IsResolvablePath(string path)
{
// File requests will blow up with an downstream exception in GetRequiredPublishedSnapshot, which fails due to an UmbracoContext
// not being available for what's considered a static file request.
// See: https://github.com/umbraco/Umbraco-CMS/issues/19051
// Given a URL segment and hence route can't contain a period, we can safely assume that if the last segment of the path contains
// a period, it's a file request and should return null here.
if (IsFileRequest(path))
{
return false;
}
return true;
}
private static bool IsFileRequest(string path) => path
.Split('/', StringSplitOptions.RemoveEmptyEntries)
.LastOrDefault()?
.Contains('.') is true;
public virtual IPublishedContent? ResolveContentPath(string path)
{
path = path.EnsureStartsWith("/");
@@ -4,5 +4,8 @@ namespace Umbraco.Cms.Core.DeliveryApi;
public interface IApiContentPathResolver
{
[Obsolete("No longer used in V15. Scheduled for removal in V15.")]
bool IsResolvablePath(string path) => true;
IPublishedContent? ResolveContentPath(string path);
}
@@ -0,0 +1,12 @@
namespace Umbraco.Cms.Core.DeliveryApi;
public interface ICurrentMemberClaimsProvider
{
/// <summary>
/// Retrieves the claims for the currently logged in member.
/// </summary>
/// <remarks>
/// This is used by the OIDC user info endpoint to supply "current user" info.
/// </remarks>
Task<Dictionary<string, object>> GetClaimsAsync();
}
@@ -0,0 +1,6 @@
namespace Umbraco.Cms.Core.DeliveryApi;
public class NoopCurrentMemberClaimsProvider : ICurrentMemberClaimsProvider
{
public Task<Dictionary<string, object>> GetClaimsAsync() => Task.FromResult(new Dictionary<string, object>());
}
@@ -27,7 +27,9 @@ public class RootDynamicRootOriginFinder : IDynamicRootOriginFinder
return null;
}
var entity = _entityService.Get(query.Context.ParentKey);
// when creating new content, CurrentKey will be null - fallback to using ParentKey
Guid entityKey = query.Context.CurrentKey ?? query.Context.ParentKey;
var entity = _entityService.Get(entityKey);
if (entity is null || _allowedObjectTypes.Contains(entity.NodeObjectType) is false)
{
@@ -20,12 +20,14 @@ public class SiteDynamicRootOriginFinder : RootDynamicRootOriginFinder
public override Guid? FindOriginKey(DynamicRootNodeQuery query)
{
if (query.OriginAlias != SupportedOriginType || query.Context.CurrentKey.HasValue is false)
if (query.OriginAlias != SupportedOriginType)
{
return null;
}
IEntitySlim? entity = _entityService.Get(query.Context.CurrentKey.Value);
// when creating new content, CurrentKey will be null - fallback to using ParentKey
Guid entityKey = query.Context.CurrentKey ?? query.Context.ParentKey;
IEntitySlim? entity = _entityService.Get(entityKey);
if (entity is null || entity.NodeObjectType != Constants.ObjectTypes.Document)
{
return null;
@@ -1469,6 +1469,7 @@ Mange hilsner fra Umbraco robotten
<key alias="publishWithMissingDomain">Der er ikke noget domæne konfigureret for %0%, kontakt vensligst en
administrator, se loggen for mere information
</key>
<key alias="publishWithNoUrl">Dokumentet har ikke nogen URL, muligvis grundet en kollision med et andet dokuments navn. Flere detaljer kan ses under Info.</key>
<key alias="copySuccessMessage">Dit systems information er blevet kopieret til udklipsholderen</key>
<key alias="cannotCopyInformation">Kunne desværre ikke kopiere dit systems information til udklipsholderen</key>
<key alias="webhookSaved">Webhook gemt</key>
File diff suppressed because it is too large Load Diff
@@ -167,6 +167,7 @@
<key alias="move">Media moved</key>
<key alias="copy">Media copied</key>
<key alias="save">Media saved</key>
<key alias="new">Media created</key>
</area>
<area alias="auditTrails">
<key alias="atViewingFor">Viewing for</key>
@@ -190,6 +191,7 @@
<key alias="smallPublishVariant">Publish</key>
<key alias="smallMove">Move</key>
<key alias="smallSave">Save</key>
<key alias="smallNew">New</key>
<key alias="smallSaveVariant">Save</key>
<key alias="smallDelete">Delete</key>
<key alias="smallUnpublish">Unpublish</key>
@@ -292,6 +294,7 @@
Content.
</key>
<key alias="nestedContentDeleteAllItems">Are you sure you want to delete all items?</key>
<key alias="nestedContentDeleteItem">Are you sure you want to delete this item?</key>
<key alias="nestedContentNoContentTypes">No Content Types are configured for this property.</key>
<key alias="nestedContentAddElementType">Add Element Type</key>
<key alias="nestedContentSelectElementTypeModalTitle">Select Element Type</key>
@@ -329,6 +332,7 @@
<key alias="schedulePublishHelp">Select the date and time to publish and/or unpublish the content item.</key>
<key alias="createEmpty">Create new</key>
<key alias="createFromClipboard">Paste from clipboard</key>
<key alias="removeItem">Remove item</key>
<key alias="nodeIsInTrash">This item is in the Recycle Bin</key>
<key alias="noProperties">No content can be added for this item</key>
<key alias="variantSaveNotAllowed">Save is not allowed</key>
@@ -1700,6 +1704,7 @@ To manage your website, simply open the Umbraco backoffice and start adding cont
<key alias="publishWithMissingDomain">There is no domain configured for %0%, please contact an administrator, see
log for more information
</key>
<key alias="publishWithNoUrl">The document does not have a URL, possibly due to a naming collision with another document. More details can be found under Info.</key>
<key alias="copySuccessMessage">Your system information has successfully been copied to the clipboard</key>
<key alias="cannotCopyInformation">Could not copy your system information to the clipboard</key>
<key alias="webhookSaved">Webhook saved</key>
@@ -165,6 +165,7 @@
<key alias="move">Media moved</key>
<key alias="copy">Media copied</key>
<key alias="save">Media saved</key>
<key alias="new">Media created</key>
</area>
<area alias="auditTrails">
<key alias="atViewingFor">Viewing for</key>
@@ -189,6 +190,7 @@
<key alias="smallPublishVariant">Publish</key>
<key alias="smallMove">Move</key>
<key alias="smallSave">Save</key>
<key alias="smallNew">New</key>
<key alias="smallSaveVariant">Save</key>
<key alias="smallDelete">Delete</key>
<key alias="smallUnpublish">Unpublish</key>
@@ -288,6 +290,7 @@
<![CDATA[<a href="https://docs.umbraco.com/umbraco-cms/fundamentals/data/scheduled-publishing#timezones" target="_blank" rel="noopener">What does this mean?</a>]]></key>
<key alias="nestedContentDeleteItem">Are you sure you want to delete this item?</key>
<key alias="nestedContentDeleteAllItems">Are you sure you want to delete all items?</key>
<key alias="nestedContentDeleteItem">Are you sure you want to delete this item?</key>
<key alias="nestedContentEditorNotSupported">Property %0% uses editor %1% which is not supported by Nested
Content.
</key>
@@ -328,6 +331,7 @@
<key alias="schedulePublishHelp">Select the date and time to publish and/or unpublish the content item.</key>
<key alias="createEmpty">Create new</key>
<key alias="createFromClipboard">Paste from clipboard</key>
<key alias="removeItem">Remove item</key>
<key alias="nodeIsInTrash">This item is in the Recycle Bin</key>
<key alias="variantSaveNotAllowed">Save is not allowed</key>
<key alias="variantPublishNotAllowed">Publish is not allowed</key>
@@ -1732,6 +1736,7 @@ To manage your website, simply open the Umbraco backoffice and start adding cont
<key alias="publishWithMissingDomain">There is no domain configured for %0%, please contact an administrator, see
log for more information
</key>
<key alias="publishWithNoUrl">The document does not have a URL, possibly due to a naming collision with another document. More details can be found under Info.</key>
<key alias="preventCleanupEnableError">An error occurred while enabling version cleanup for %0%</key>
<key alias="preventCleanupDisableError">An error occurred while disabling version cleanup for %0%</key>
<key alias="copySuccessMessage">Your system information has successfully been copied to the clipboard</key>
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -5,7 +5,7 @@
@using Umbraco.Extensions
@{
var isLoggedIn = Context.User?.Identity?.IsAuthenticated ?? false;
var isLoggedIn = Context.User.GetMemberIdentity()?.IsAuthenticated ?? false;
var logoutModel = new PostRedirectModel();
// You can modify this to redirect to a different URL instead of the current one
logoutModel.RedirectUrl = null;
@@ -15,7 +15,7 @@
{
<div class="login-status">
<p>Welcome back <strong>@Context?.User?.Identity?.Name</strong>!</p>
<p>Welcome back <strong>@Context.User?.GetMemberIdentity()?.Name</strong>!</p>
@using (Html.BeginUmbracoForm<UmbLoginStatusController>("HandleLogout", new { RedirectUrl = logoutModel.RedirectUrl }))
{
@@ -330,7 +330,10 @@ public static class ClaimsIdentityExtensions
var firstValue = identity.FindFirstValue(ClaimTypes.NameIdentifier);
if (firstValue is not null)
{
return int.Parse(firstValue, CultureInfo.InvariantCulture);
if (int.TryParse(firstValue, CultureInfo.InvariantCulture, out var id))
{
return id;
}
}
return null;
+32 -6
View File
@@ -1,15 +1,17 @@
// Copyright (c) Umbraco.
// See LICENSE for more details.
using System.Diagnostics.CodeAnalysis;
namespace Umbraco.Extensions;
public static class IntExtensions
{
/// <summary>
/// Does something 'x' amount of times
/// Does something 'x' amount of times.
/// </summary>
/// <param name="n"></param>
/// <param name="action"></param>
/// <param name="n">Number of times to execute the action.</param>
/// <param name="action">The action to execute.</param>
public static void Times(this int n, Action<int> action)
{
for (var i = 0; i < n; i++)
@@ -19,11 +21,11 @@ public static class IntExtensions
}
/// <summary>
/// Creates a Guid based on an integer value
/// Creates a Guid based on an integer value.
/// </summary>
/// <param name="value"><see cref="int" /> value to convert</param>
/// <param name="value">The <see cref="int" /> value to convert.</param>
/// <returns>
/// <see cref="Guid" />
/// The converted <see cref="Guid" />.
/// </returns>
public static Guid ToGuid(this int value)
{
@@ -31,4 +33,28 @@ public static class IntExtensions
BitConverter.GetBytes(value).CopyTo(bytes, 0);
return new Guid(bytes);
}
/// <summary>
/// Restores a GUID previously created from an integer value using <see cref="ToGuid" />.
/// </summary>
/// <param name="value">The <see cref="Guid" /> value to convert.</param>
/// <param name="result">The converted <see cref="int" />.</param>
/// <returns>
/// True if the <see cref="int" /> value could be created, otherwise false.
/// </returns>
/// <remarks>
/// This is used with Umbraco entities that only have integer references in the database (e.g. users).
/// </remarks>
public static bool TryParseFromGuid(Guid value, [NotNullWhen(true)] out int? result)
{
if (value.ToString().EndsWith("-0000-0000-0000-000000000000") is false)
{
// We have a proper GUID, not one converted from an integer.
result = null;
return false;
}
result = BitConverter.ToInt32(value.ToByteArray());
return true;
}
}
+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>
+15
View File
@@ -7,6 +7,21 @@ public sealed class AuditItem : EntityBase, IAuditItem
/// <summary>
/// Initializes a new instance of the <see cref="AuditItem" /> class.
/// </summary>
public AuditItem(int objectId, AuditType type, int userId, string? entityType, DateTime createDate, string? comment = null, string? parameters = null)
{
DisableChangeTracking();
Id = objectId;
Comment = comment;
AuditType = type;
UserId = userId;
EntityType = entityType;
Parameters = parameters;
CreateDate = createDate;
EnableChangeTracking();
}
public AuditItem(int objectId, AuditType type, int userId, string? entityType, string? comment = null, string? parameters = null)
{
DisableChangeTracking();
@@ -1,69 +1,69 @@
namespace Umbraco.Cms.Core.Models.ContentEditing;
/// <summary>
/// The action associated with saving a content item
/// The action associated with saving a content item.
/// </summary>
public enum ContentSaveAction
{
/// <summary>
/// Saves the content item, no publish
/// Saves the content item, no publish.
/// </summary>
Save = 0,
/// <summary>
/// Creates a new content item
/// Creates a new content item.
/// </summary>
SaveNew = 1,
/// <summary>
/// Saves and publishes the content item
/// Saves and publishes the content item.
/// </summary>
Publish = 2,
/// <summary>
/// Creates and publishes a new content item
/// Creates and publishes a new content item.
/// </summary>
PublishNew = 3,
/// <summary>
/// Saves and sends publish notification
/// Saves and sends publish notification.
/// </summary>
SendPublish = 4,
/// <summary>
/// Creates and sends publish notification
/// Creates and sends publish notification.
/// </summary>
SendPublishNew = 5,
/// <summary>
/// Saves and schedules publishing
/// Saves and schedules publishing.
/// </summary>
Schedule = 6,
/// <summary>
/// Creates and schedules publishing
/// Creates and schedules publishing.
/// </summary>
ScheduleNew = 7,
/// <summary>
/// Saves and publishes the content item including all descendants that have a published version
/// Saves and publishes the content item including all descendants that have a published version.
/// </summary>
PublishWithDescendants = 8,
/// <summary>
/// Creates and publishes the content item including all descendants that have a published version
/// Creates and publishes the new content item including all descendants that have a published version.
/// </summary>
PublishWithDescendantsNew = 9,
/// <summary>
/// Saves and publishes the content item including all descendants regardless of whether they have a published version
/// or not
/// or not.
/// </summary>
PublishWithDescendantsForce = 10,
/// <summary>
/// Creates and publishes the content item including all descendants regardless of whether they have a published
/// version or not
/// Creates and publishes the new content item including all descendants regardless of whether they have a published
/// version or not.
/// </summary>
PublishWithDescendantsForceNew = 11,
}
@@ -25,6 +25,7 @@ public class PropertyTypeBasic
[Required]
[RegularExpression(@"^([a-zA-Z]\w.*)$", ErrorMessage = "Invalid alias")]
[MaxLength(255, ErrorMessage = "Alias is too long")]
[DataMember(Name = "alias")]
public string Alias { get; set; } = null!;
@@ -0,0 +1,28 @@
namespace Umbraco.Cms.Core.Models;
/// <summary>
/// Describes the options available with publishing a content branch for force publishing.
/// </summary>
[Flags]
public enum PublishBranchFilter
{
/// <summary>
/// The default behavior is to publish only the published content that has changed.
/// </summary>
Default = 0,
/// <summary>
/// For publishing a branch, publish all changed content, including content that is not published.
/// </summary>
IncludeUnpublished = 1,
/// <summary>
/// For publishing a branch, force republishing of all published content, including content that has not changed.
/// </summary>
ForceRepublish = 2,
/// <summary>
/// For publishing a branch, publish all content, including content that is not published and content that has not changed.
/// </summary>
All = IncludeUnpublished | ForceRepublish,
}
@@ -0,0 +1,28 @@
namespace Umbraco.Cms.Core.Models;
/// <summary>
/// Specifies options for publishing notifcations when saving.
/// </summary>
[Flags]
public enum PublishNotificationSaveOptions
{
/// <summary>
/// Do not publish any notifications.
/// </summary>
None = 0,
/// <summary>
/// Only publish the saving notification.
/// </summary>
Saving = 1,
/// <summary>
/// Only publish the saved notification.
/// </summary>
Saved = 2,
/// <summary>
/// Publish all the notifications.
/// </summary>
All = Saving | Saved,
}
@@ -32,4 +32,14 @@ public class ContentTreeChangeNotification : TreeChangeNotification<IContent>
: base(new TreeChange<IContent>(target, changeTypes), messages)
{
}
public ContentTreeChangeNotification(
IContent target,
TreeChangeTypes changeTypes,
IEnumerable<string>? publishedCultures,
IEnumerable<string>? unpublishedCultures,
EventMessages messages)
: base(new TreeChange<IContent>(target, changeTypes, publishedCultures, unpublishedCultures), messages)
{
}
}
@@ -3,23 +3,29 @@ using Umbraco.Cms.Core.Security;
namespace Umbraco.Cms.Core.Persistence.Repositories;
/// <summary>
/// Repository for external logins with Guid as key, so it can be shared for members and users
/// Repository for external logins with Guid as key, so it can be shared for members and users.
/// </summary>
public interface IExternalLoginWithKeyRepository : IReadWriteQueryRepository<int, IIdentityUserLogin>,
IQueryRepository<IIdentityUserToken>
{
/// <summary>
/// Replaces all external login providers for the user/member key
/// Replaces all external login providers for the user/member key.
/// </summary>
void Save(Guid userOrMemberKey, IEnumerable<IExternalLogin> logins);
/// <summary>
/// Replaces all external login provider tokens for the providers specified for the user/member key
/// Replaces all external login provider tokens for the providers specified for the user/member key.
/// </summary>
void Save(Guid userOrMemberKey, IEnumerable<IExternalLoginToken> tokens);
/// <summary>
/// Deletes all external logins for the specified the user/member key
/// Deletes all external logins for the specified the user/member key.
/// </summary>
void DeleteUserLogins(Guid userOrMemberKey);
/// <summary>
/// Deletes external logins that aren't associated with the current collection of providers.
/// </summary>
/// <param name="currentLoginProviders">The names of the currently configured providers.</param>
void DeleteUserLoginsForRemovedProviders(IEnumerable<string> currentLoginProviders) { }
}
@@ -38,4 +38,11 @@ public interface IMemberRepository : IContentRepository<int, IMember>
/// <param name="query"></param>
/// <returns></returns>
int GetCountByQuery(IQuery<IMember>? query);
/// <summary>
/// Saves only the properties related to login for the member, using an optimized, non-locking update.
/// </summary>
/// <param name="member">The member to update.</param>
/// <returns>Used to avoid the full save of the member object after a login operation.</returns>
Task UpdateLoginPropertiesAsync(IMember member) => Task.CompletedTask;
}
@@ -1,4 +1,4 @@
using System.Linq.Expressions;
using System.Linq.Expressions;
using Umbraco.Cms.Core.Models.Membership;
using Umbraco.Cms.Core.Persistence.Querying;
@@ -109,5 +109,26 @@ public interface IUserRepository : IReadWriteQueryRepository<int, IUser>
void ClearLoginSession(Guid sessionId);
/// <summary>
/// Gets a page of users, ordered by Id and starting from the provided Id.
/// </summary>
/// <param name="id">The user Id to start retrieving users from.</param>
/// <param name="count">The number of users to return.</param>
/// <returns>A page of <see cref="IUser"/> instances.</returns>
[Obsolete("No longer used in Umbraco. Scheduled for removal in Umbraco 18.")]
IEnumerable<IUser> GetNextUsers(int id, int count);
/// <summary>
/// Gets a page of approved users, ordered by Id and starting from the provided Id.
/// </summary>
/// <param name="id">The user Id to start retrieving users from.</param>
/// <param name="count">The number of users to return.</param>
/// <returns>A page of <see cref="IUser"/> instances.</returns>
IEnumerable<IUser> GetNextApprovedUsers(int id, int count) => Enumerable.Empty<IUser>();
/// <summary>
/// Invalidates sessions for users that aren't associated with the current collection of providers.
/// </summary>
/// <param name="currentProviderKeys">The keys for the currently configured providers.</param>
void InvalidateSessionsForRemovedProviders(IEnumerable<string> currentProviderKeys) { }
}
@@ -31,6 +31,12 @@ public interface IPublishedSnapshotService : IDisposable
/// </remarks>
IPublishedSnapshot CreatePublishedSnapshot(string? previewToken);
/// <summary>
/// Indicates if the database cache is in the process of being rebuilt.
/// </summary>
/// <returns></returns>
bool IsRebuilding() => false;
/// <summary>
/// Rebuilds internal database caches (but does not reload).
/// </summary>
@@ -61,6 +67,38 @@ public interface IPublishedSnapshotService : IDisposable
IReadOnlyCollection<int>? mediaTypeIds = null,
IReadOnlyCollection<int>? memberTypeIds = null);
/// <summary>
/// Rebuilds internal database caches (but does not reload).
/// </summary>
/// <param name="contentTypeIds">
/// If not null will process content for the matching content types, if empty will process all
/// content
/// </param>
/// <param name="mediaTypeIds">
/// If not null will process content for the matching media types, if empty will process all
/// media
/// </param>
/// <param name="memberTypeIds">
/// If not null will process content for the matching members types, if empty will process all
/// members
/// </param>
/// <param name="useBackgroundThread">Flag indicating whether to use a background thread for the operation and immediately return to the caller.</param>
/// <remarks>
/// <para>
/// Forces the snapshot service to rebuild its internal database caches. For instance, some caches
/// may rely on a database table to store pre-serialized version of documents.
/// </para>
/// <para>
/// This does *not* reload the caches. Caches need to be reloaded, for instance via
/// <see cref="DistributedCache" /> RefreshAllPublishedSnapshot method.
/// </para>
/// </remarks>
void Rebuild(
bool useBackgroundThread,
IReadOnlyCollection<int>? contentTypeIds = null,
IReadOnlyCollection<int>? mediaTypeIds = null,
IReadOnlyCollection<int>? memberTypeIds = null) => Rebuild(contentTypeIds, mediaTypeIds, memberTypeIds);
/// <summary>
/// Rebuilds all internal database caches (but does not reload).
@@ -77,6 +115,22 @@ public interface IPublishedSnapshotService : IDisposable
/// </remarks>
void RebuildAll() => Rebuild(Array.Empty<int>(), Array.Empty<int>(), Array.Empty<int>());
/// <summary>
/// Rebuilds all internal database caches (but does not reload).
/// </summary>
/// <param name="useBackgroundThread">Flag indicating whether to use a background thread for the operation and immediately return to the caller.</param>
/// <remarks>
/// <para>
/// Forces the snapshot service to rebuild its internal database caches. For instance, some caches
/// may rely on a database table to store pre-serialized version of documents.
/// </para>
/// <para>
/// This does *not* reload the caches. Caches need to be reloaded, for instance via
/// <see cref="DistributedCache" /> RefreshAllPublishedSnapshot method.
/// </para>
/// </remarks>
void RebuildAll(bool useBackgroundThread) => Rebuild(useBackgroundThread, Array.Empty<int>(), Array.Empty<int>(), Array.Empty<int>());
/* An IPublishedCachesService implementation can rely on transaction-level events to update
* its internal, database-level data, as these events are purely internal. However, it cannot
* rely on cache refreshers CacheUpdated events to update itself, as these events are external
+1 -1
View File
@@ -33,7 +33,7 @@ public interface ITagQuery
/// <summary>
/// Gets all document tags.
/// </summary>
/// /// <remarks>
/// <remarks>
/// If no culture is specified, it retrieves tags with an invariant culture.
/// If a culture is specified, it only retrieves tags for that culture.
/// Use "*" to retrieve tags for all cultures.
@@ -148,7 +148,10 @@ public class UmbracoRequestPaths
/// <summary>
/// Checks if the current uri is an install request
/// </summary>
public bool IsInstallerRequest(string absPath) => absPath.InvariantStartsWith(_installPath);
public bool IsInstallerRequest(string absPath) =>
absPath.InvariantEquals(_installPath)
|| absPath.InvariantStartsWith(_installPath.EnsureEndsWith('/'))
|| absPath.InvariantStartsWith(_installPath.EnsureEndsWith('?'));
/// <summary>
/// Rudimentary check to see if it's not a server side request
+6
View File
@@ -111,6 +111,12 @@ public sealed class UriUtility
if (path != "/")
{
path = path.TrimEnd(Constants.CharArrays.ForwardSlash);
// perform fallback to root if the path was all slashes (i.e. https://some.where//////)
if (path == string.Empty)
{
path = "/";
}
}
return uri.Rewrite(path);
@@ -8,10 +8,22 @@ public class TreeChange<TItem>
ChangeTypes = changeTypes;
}
public TreeChange(TItem changedItem, TreeChangeTypes changeTypes, IEnumerable<string>? publishedCultures, IEnumerable<string>? unpublishedCultures)
{
Item = changedItem;
ChangeTypes = changeTypes;
PublishedCultures = publishedCultures;
UnpublishedCultures = unpublishedCultures;
}
public TItem Item { get; }
public TreeChangeTypes ChangeTypes { get; }
public IEnumerable<string>? PublishedCultures { get; }
public IEnumerable<string>? UnpublishedCultures { get; }
public EventArgs ToEventArgs() => new EventArgs(this);
public class EventArgs : System.EventArgs
+68 -37
View File
@@ -1595,7 +1595,12 @@ public class ContentService : RepositoryService, IContentService
// events and audit
scope.Notifications.Publish(
new ContentUnpublishedNotification(content, eventMessages).WithState(notificationState));
scope.Notifications.Publish(new ContentTreeChangeNotification(content, TreeChangeTypes.RefreshBranch, eventMessages));
scope.Notifications.Publish(new ContentTreeChangeNotification(
content,
TreeChangeTypes.RefreshBranch,
variesByCulture ? culturesPublishing.IsCollectionEmpty() ? null : culturesPublishing : null,
variesByCulture ? culturesUnpublishing.IsCollectionEmpty() ? null : culturesUnpublishing : ["*"],
eventMessages));
if (culturesUnpublishing != null)
{
@@ -1654,7 +1659,12 @@ public class ContentService : RepositoryService, IContentService
if (!branchOne)
{
scope.Notifications.Publish(
new ContentTreeChangeNotification(content, changeType, eventMessages));
new ContentTreeChangeNotification(
content,
changeType,
variesByCulture ? culturesPublishing.IsCollectionEmpty() ? null : culturesPublishing : ["*"],
variesByCulture ? culturesUnpublishing.IsCollectionEmpty() ? null : culturesUnpublishing : null,
eventMessages));
scope.Notifications.Publish(
new ContentPublishedNotification(content, eventMessages).WithState(notificationState));
}
@@ -1953,17 +1963,14 @@ public class ContentService : RepositoryService, IContentService
}
// utility 'ShouldPublish' func used by SaveAndPublishBranch
private HashSet<string>? SaveAndPublishBranch_ShouldPublish(ref HashSet<string>? cultures, string c, bool published, bool edited, bool isRoot, bool force)
private HashSet<string>? SaveAndPublishBranch_ShouldPublish(ref HashSet<string>? cultures, string c, bool published, bool edited, bool isRoot, PublishBranchFilter publishBranchFilter)
{
// if published, republish
if (published)
{
if (cultures == null)
{
cultures = new HashSet<string>(); // empty means 'already published'
}
cultures ??= []; // empty means 'already published'
if (edited)
if (edited || publishBranchFilter.HasFlag(PublishBranchFilter.ForceRepublish))
{
cultures.Add(c); // <culture> means 'republish this culture'
}
@@ -1972,15 +1979,12 @@ public class ContentService : RepositoryService, IContentService
}
// if not published, publish if force/root else do nothing
if (!force && !isRoot)
if (!publishBranchFilter.HasFlag(PublishBranchFilter.IncludeUnpublished) && !isRoot)
{
return cultures; // null means 'nothing to do'
}
if (cultures == null)
{
cultures = new HashSet<string>();
}
cultures ??= [];
cultures.Add(c); // <culture> means 'publish this culture'
return cultures;
@@ -1988,6 +1992,10 @@ public class ContentService : RepositoryService, IContentService
/// <inheritdoc />
public IEnumerable<PublishResult> SaveAndPublishBranch(IContent content, bool force, string culture = "*", int userId = Constants.Security.SuperUserId)
=> SaveAndPublishBranch(content, force ? PublishBranchFilter.IncludeUnpublished : PublishBranchFilter.Default, culture, userId);
/// <inheritdoc />
public IEnumerable<PublishResult> SaveAndPublishBranch(IContent content, PublishBranchFilter publishBranchFilter, string culture = "*", int userId = Constants.Security.SuperUserId)
{
// note: EditedValue and PublishedValue are objects here, so it is important to .Equals()
// and not to == them, else we would be comparing references, and that is a bad thing
@@ -2006,13 +2014,13 @@ public class ContentService : RepositoryService, IContentService
// invariant content type
if (!c.ContentType.VariesByCulture())
{
return SaveAndPublishBranch_ShouldPublish(ref culturesToPublish, "*", c.Published, c.Edited, isRoot, force);
return SaveAndPublishBranch_ShouldPublish(ref culturesToPublish, "*", c.Published, c.Edited, isRoot, publishBranchFilter);
}
// variant content type, specific culture
if (culture != "*")
{
return SaveAndPublishBranch_ShouldPublish(ref culturesToPublish, culture, c.IsCulturePublished(culture), c.IsCultureEdited(culture), isRoot, force);
return SaveAndPublishBranch_ShouldPublish(ref culturesToPublish, culture, c.IsCulturePublished(culture), c.IsCultureEdited(culture), isRoot, publishBranchFilter);
}
// variant content type, all cultures
@@ -2022,23 +2030,27 @@ public class ContentService : RepositoryService, IContentService
// others will have to 'republish this culture'
foreach (var x in c.AvailableCultures)
{
SaveAndPublishBranch_ShouldPublish(ref culturesToPublish, x, c.IsCulturePublished(x), c.IsCultureEdited(x), isRoot, force);
SaveAndPublishBranch_ShouldPublish(ref culturesToPublish, x, c.IsCulturePublished(x), c.IsCultureEdited(x), isRoot, publishBranchFilter);
}
return culturesToPublish;
}
// if not published, publish if force/root else do nothing
return force || isRoot
// if not published, publish if forcing unpublished/root else do nothing
return publishBranchFilter.HasFlag(PublishBranchFilter.IncludeUnpublished) || isRoot
? new HashSet<string> { "*" } // "*" means 'publish all'
: null; // null means 'nothing to do'
}
return SaveAndPublishBranch(content, force, ShouldPublish, SaveAndPublishBranch_PublishCultures, userId);
return SaveAndPublishBranch(content, ShouldPublish, SaveAndPublishBranch_PublishCultures, userId);
}
/// <inheritdoc />
public IEnumerable<PublishResult> SaveAndPublishBranch(IContent content, bool force, string[] cultures, int userId = Constants.Security.SuperUserId)
=> SaveAndPublishBranch(content, force ? PublishBranchFilter.IncludeUnpublished : PublishBranchFilter.Default, cultures, userId);
/// <inheritdoc />
public IEnumerable<PublishResult> SaveAndPublishBranch(IContent content, PublishBranchFilter publishBranchFilter, string[] cultures, int userId = Constants.Security.SuperUserId)
{
// note: EditedValue and PublishedValue are objects here, so it is important to .Equals()
// and not to == them, else we would be comparing references, and that is a bad thing
@@ -2054,7 +2066,7 @@ public class ContentService : RepositoryService, IContentService
// invariant content type
if (!c.ContentType.VariesByCulture())
{
return SaveAndPublishBranch_ShouldPublish(ref culturesToPublish, "*", c.Published, c.Edited, isRoot, force);
return SaveAndPublishBranch_ShouldPublish(ref culturesToPublish, "*", c.Published, c.Edited, isRoot, publishBranchFilter);
}
// variant content type, specific cultures
@@ -2064,24 +2076,23 @@ public class ContentService : RepositoryService, IContentService
// others will have to 'republish this culture'
foreach (var x in cultures)
{
SaveAndPublishBranch_ShouldPublish(ref culturesToPublish, x, c.IsCulturePublished(x), c.IsCultureEdited(x), isRoot, force);
SaveAndPublishBranch_ShouldPublish(ref culturesToPublish, x, c.IsCulturePublished(x), c.IsCultureEdited(x), isRoot, publishBranchFilter);
}
return culturesToPublish;
}
// if not published, publish if force/root else do nothing
return force || isRoot
// if not published, publish if forcing unpublished/root else do nothing
return publishBranchFilter.HasFlag(PublishBranchFilter.IncludeUnpublished) || isRoot
? new HashSet<string>(cultures) // means 'publish specified cultures'
: null; // null means 'nothing to do'
}
return SaveAndPublishBranch(content, force, ShouldPublish, SaveAndPublishBranch_PublishCultures, userId);
return SaveAndPublishBranch(content, ShouldPublish, SaveAndPublishBranch_PublishCultures, userId);
}
internal IEnumerable<PublishResult> SaveAndPublishBranch(
IContent document,
bool force,
Func<IContent, HashSet<string>?> shouldPublish,
Func<IContent, HashSet<string>, IReadOnlyCollection<ILanguage>, bool> publishCultures,
int userId = Constants.Security.SuperUserId)
@@ -2118,7 +2129,8 @@ public class ContentService : RepositoryService, IContentService
}
// deal with the branch root - if it fails, abort
PublishResult? result = SaveAndPublishBranchItem(scope, document, shouldPublish, publishCultures, true, publishedDocuments, eventMessages, userId, allLangs, out IDictionary<string, object?> notificationState);
HashSet<string>? culturesToPublish = shouldPublish(document);
PublishResult? result = SaveAndPublishBranchItem(scope, document, culturesToPublish, publishCultures, true, publishedDocuments, eventMessages, userId, allLangs, out IDictionary<string, object?> notificationState);
if (result != null)
{
results.Add(result);
@@ -2128,6 +2140,8 @@ public class ContentService : RepositoryService, IContentService
}
}
HashSet<string> culturesPublished = culturesToPublish ?? [];
// deal with descendants
// if one fails, abort its branch
var exclude = new HashSet<int>();
@@ -2153,12 +2167,14 @@ public class ContentService : RepositoryService, IContentService
}
// no need to check path here, parent has to be published here
result = SaveAndPublishBranchItem(scope, d, shouldPublish, publishCultures, false, publishedDocuments, eventMessages, userId, allLangs, out _);
culturesToPublish = shouldPublish(d);
result = SaveAndPublishBranchItem(scope, d, culturesToPublish, publishCultures, false, publishedDocuments, eventMessages, userId, allLangs, out _);
if (result != null)
{
results.Add(result);
if (result.Success)
{
culturesPublished.UnionWith(culturesToPublish ?? []);
continue;
}
}
@@ -2175,8 +2191,14 @@ public class ContentService : RepositoryService, IContentService
// trigger events for the entire branch
// (SaveAndPublishBranchOne does *not* do it)
var variesByCulture = document.ContentType.VariesByCulture();
scope.Notifications.Publish(
new ContentTreeChangeNotification(document, TreeChangeTypes.RefreshBranch, eventMessages));
new ContentTreeChangeNotification(
document,
TreeChangeTypes.RefreshBranch,
variesByCulture ? culturesPublished.IsCollectionEmpty() ? null : culturesPublished : ["*"],
null,
eventMessages));
scope.Notifications.Publish(new ContentPublishedNotification(publishedDocuments, eventMessages).WithState(notificationState));
scope.Complete();
@@ -2191,7 +2213,7 @@ public class ContentService : RepositoryService, IContentService
private PublishResult? SaveAndPublishBranchItem(
ICoreScope scope,
IContent document,
Func<IContent, HashSet<string>?> shouldPublish,
HashSet<string>? culturesToPublish,
Func<IContent, HashSet<string>, IReadOnlyCollection<ILanguage>,
bool> publishCultures,
bool isRoot,
@@ -2202,7 +2224,6 @@ public class ContentService : RepositoryService, IContentService
out IDictionary<string, object?> notificationState)
{
notificationState = new Dictionary<string, object?>();
HashSet<string>? culturesToPublish = shouldPublish(document);
// null = do not include
if (culturesToPublish == null)
@@ -2448,22 +2469,26 @@ public class ContentService : RepositoryService, IContentService
/// <param name="content">The <see cref="IContent" /> to move</param>
/// <param name="parentId">Id of the Content's new Parent</param>
/// <param name="userId">Optional Id of the User moving the Content</param>
public void Move(IContent content, int parentId, int userId = Constants.Security.SuperUserId)
public void Move(IContent content, int parentId, int userId = Constants.Security.SuperUserId) =>
AttemptMove(content, parentId, userId);
/// <inheritdoc/>
[Obsolete("Adds return type to Move method. Will be removed in V14, as the original method will be adjusted.")]
public OperationResult AttemptMove(IContent content, int parentId, int userId = Constants.Security.SuperUserId)
{
EventMessages eventMessages = EventMessagesFactory.Get();
if (content.ParentId == parentId)
{
return;
return OperationResult.Succeed(eventMessages);
}
// if moving to the recycle bin then use the proper method
if (parentId == Constants.System.RecycleBinContent)
{
MoveToRecycleBin(content, userId);
return;
return MoveToRecycleBin(content, userId);
}
EventMessages eventMessages = EventMessagesFactory.Get();
var moves = new List<(IContent, string)>();
using (ICoreScope scope = ScopeProvider.CreateCoreScope())
@@ -2482,7 +2507,7 @@ public class ContentService : RepositoryService, IContentService
if (scope.Notifications.PublishCancelable(movingNotification))
{
scope.Complete();
return; // causes rollback
return OperationResult.Cancel(eventMessages);// causes rollback
}
// if content was trashed, and since we're not moving to the recycle bin,
@@ -2517,6 +2542,8 @@ public class ContentService : RepositoryService, IContentService
scope.Complete();
}
return OperationResult.Succeed(eventMessages);
}
// MUST be called from within WriteLock
@@ -2754,6 +2781,9 @@ public class ContentService : RepositoryService, IContentService
descendantCopy.CreatorId = userId;
descendantCopy.WriterId = userId;
// since the repository relies on the dirty state to figure out whether it needs to update the sort order, we mark it dirty here
descendantCopy.SortOrder = descendantCopy.SortOrder;
// save and flush (see above)
_documentRepository.Save(descendantCopy);
@@ -3597,6 +3627,7 @@ public class ContentService : RepositoryService, IContentService
private static readonly string?[] ArrayOfOneNullString = { null };
/// <inheritdoc />
public IContent CreateContentFromBlueprint(IContent blueprint, string name, int userId = Constants.Security.SuperUserId)
{
if (blueprint == null)
+15 -1
View File
@@ -108,7 +108,7 @@ public class DomainService : RepositoryService, IDomainService
EventMessages eventMessages = EventMessagesFactory.Get();
IDomain[] domains = items.ToArray();
if (domains.Length == 0)
if (domains.Length == 0 || AreDomainsAlreadySorted(domains))
{
return OperationResult.Attempt.NoOperation(eventMessages);
}
@@ -144,4 +144,18 @@ public class DomainService : RepositoryService, IDomainService
return OperationResult.Attempt.Succeed(eventMessages);
}
private static bool AreDomainsAlreadySorted(IDomain[] domains)
{
// Check if the domains are already sorted by comparing the current sort order with what we'll set to be the new sort order.
for (int i = 0; i < domains.Length; i++)
{
if (domains[i].SortOrder != i)
{
return false;
}
}
return true;
}
}
@@ -80,4 +80,14 @@ public class ExternalLoginService : RepositoryService, IExternalLoginWithKeyServ
scope.Complete();
}
}
/// <inheritdoc />
public void DeleteUserLoginsForRemovedProviders(IEnumerable<string> currentLoginProviders)
{
using (ICoreScope scope = ScopeProvider.CreateCoreScope())
{
_externalLoginRepository.DeleteUserLoginsForRemovedProviders(currentLoginProviders);
scope.Complete();
}
}
}
@@ -1,3 +1,4 @@
using Umbraco.Cms.Core.Events;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.Membership;
using Umbraco.Cms.Core.Persistence.Querying;
@@ -55,6 +56,9 @@ public interface IContentService : IContentServiceBase<IContent>
/// <summary>
/// Creates a new content item from a blueprint.
/// </summary>
/// <remarks>Warning: If you intend to save the resulting <c>IContent</c> as a content node, you must trigger a
/// <see cref="Notifications.ContentScaffoldedNotification"/> notification to ensure that the block ids are regenerated.
/// Failing to do so could lead to caching issues.</remarks>
IContent CreateContentFromBlueprint(IContent blueprint, string name, int userId = Constants.Security.SuperUserId);
/// <summary>
@@ -315,6 +319,21 @@ public interface IContentService : IContentServiceBase<IContent>
/// </summary>
void Move(IContent content, int parentId, int userId = Constants.Security.SuperUserId);
/// <summary>
/// Attempts to move the <see cref="IContent"/> <paramref name="content"/> to under the node with id <paramref name="parentId"/>.
/// </summary>
/// <param name="content">The <see cref="IContent"/> that shall be moved.</param>
/// <param name="parentId">The id of the new parent node.</param>
/// <param name="userId">Id of the user attempting to move <paramref name="content"/>.</param>
/// <returns>Success if moving succeeded, otherwise Failed.</returns>
[Obsolete("Adds return type to Move method. Will be removed in V14, as the original method will be adjusted.")]
OperationResult
AttemptMove(IContent content, int parentId, int userId = Constants.Security.SuperUserId)
{
Move(content, parentId, userId);
return OperationResult.Succeed(new EventMessages());
}
/// <summary>
/// Copies a document.
/// </summary>
@@ -415,6 +434,7 @@ public interface IContentService : IContentServiceBase<IContent>
/// published. The root of the branch is always published, regardless of <paramref name="force" />.
/// </para>
/// </remarks>
[Obsolete("This method is not longer used as the 'force' parameter has been extended into options for publishing unpublished and re-publishing changed content. Please use the overload containing the parameter for those options instead.")]
IEnumerable<PublishResult> SaveAndPublishBranch(IContent content, bool force, string culture = "*", int userId = Constants.Security.SuperUserId);
/// <summary>
@@ -431,8 +451,47 @@ public interface IContentService : IContentServiceBase<IContent>
/// published. The root of the branch is always published, regardless of <paramref name="force" />.
/// </para>
/// </remarks>
[Obsolete("This method is not longer used as the 'force' parameter has been extended into options for publishing unpublished and re-publishing changed content. Please use the overload containing the parameter for those options instead.")]
IEnumerable<PublishResult> SaveAndPublishBranch(IContent content, bool force, string[] cultures, int userId = Constants.Security.SuperUserId);
/// <summary>
/// Saves and publishes a document branch.
/// </summary>
/// <param name="content">The root document.</param>
/// <param name="publishBranchFilter">A value indicating options for force publishing unpublished or re-publishing unchanged content.</param>
/// <param name="culture">A culture, or "*" for all cultures.</param>
/// <param name="userId">The identifier of the user performing the operation.</param>
/// <remarks>
/// <para>
/// Unless specified, all cultures are re-published. Otherwise, one culture can be specified. To act on more
/// than one culture, see the other overloads of this method.
/// </para>
/// <para>
/// The root of the branch is always published, regardless of <paramref name="publishBranchFilter" />.
/// </para>
/// </remarks>
IEnumerable<PublishResult> SaveAndPublishBranch(IContent content, PublishBranchFilter publishBranchFilter, string culture = "*", int userId = Constants.Security.SuperUserId)
#pragma warning disable CS0618 // Type or member is obsolete
=> SaveAndPublishBranch(content, publishBranchFilter.HasFlag(PublishBranchFilter.IncludeUnpublished), culture, userId);
#pragma warning restore CS0618 // Type or member is obsolete
/// <summary>
/// Saves and publishes a document branch.
/// </summary>
/// <param name="content">The root document.</param>
/// <param name="publishBranchFilter">A value indicating options for force publishing unpublished or re-publishing unchanged content.</param>
/// <param name="cultures">The cultures to publish.</param>
/// <param name="userId">The identifier of the user performing the operation.</param>
/// <remarks>
/// <para>
/// The root of the branch is always published, regardless of <paramref name="publishBranchFilter" />.
/// </para>
/// </remarks>
IEnumerable<PublishResult> SaveAndPublishBranch(IContent content, PublishBranchFilter publishBranchFilter, string[] cultures, int userId = Constants.Security.SuperUserId)
#pragma warning disable CS0618 // Type or member is obsolete
=> SaveAndPublishBranch(content, publishBranchFilter.HasFlag(PublishBranchFilter.IncludeUnpublished), cultures, userId);
#pragma warning restore CS0618 // Type or member is obsolete
///// <summary>
///// Saves and publishes a document branch.
///// </summary>
@@ -5,47 +5,53 @@ namespace Umbraco.Cms.Core.Services;
public interface IExternalLoginWithKeyService : IService
{
/// <summary>
/// Returns all user logins assigned
/// Returns all user logins assigned.
/// </summary>
IEnumerable<IIdentityUserLogin> GetExternalLogins(Guid userOrMemberKey);
/// <summary>
/// Returns all user login tokens assigned
/// Returns all user login tokens assigned.
/// </summary>
IEnumerable<IIdentityUserToken> GetExternalLoginTokens(Guid userOrMemberKey);
/// <summary>
/// Returns all logins matching the login info - generally there should only be one but in some cases
/// there might be more than one depending on if an administrator has been editing/removing members
/// there might be more than one depending on if an administrator has been editing/removing members.
/// </summary>
IEnumerable<IIdentityUserLogin> Find(string loginProvider, string providerKey);
/// <summary>
/// Saves the external logins associated with the user
/// Saves the external logins associated with the user.
/// </summary>
/// <param name="userOrMemberKey">
/// The user or member key associated with the logins
/// The user or member key associated with the logins.
/// </param>
/// <param name="logins"></param>
/// <remarks>
/// This will replace all external login provider information for the user
/// This will replace all external login provider information for the user.
/// </remarks>
void Save(Guid userOrMemberKey, IEnumerable<IExternalLogin> logins);
/// <summary>
/// Saves the external login tokens associated with the user
/// Saves the external login tokens associated with the user.
/// </summary>
/// <param name="userOrMemberKey">
/// The user or member key associated with the logins
/// The user or member key associated with the logins.
/// </param>
/// <param name="tokens"></param>
/// <remarks>
/// This will replace all external login tokens for the user
/// This will replace all external login tokens for the user.
/// </remarks>
void Save(Guid userOrMemberKey, IEnumerable<IExternalLoginToken> tokens);
/// <summary>
/// Deletes all user logins - normally used when a member is deleted
/// Deletes all user logins - normally used when a member is deleted.
/// </summary>
void DeleteUserLogins(Guid userOrMemberKey);
/// <summary>
/// Deletes external logins that aren't associated with the current collection of providers.
/// </summary>
/// <param name="currentLoginProviders">The names of the currently configured providers.</param>
void DeleteUserLoginsForRemovedProviders(IEnumerable<string> currentLoginProviders) { }
}
@@ -210,6 +210,21 @@ public interface IMemberService : IMembershipMemberService
/// </returns>
IMember? GetById(int id);
/// <summary>
/// Get an list of <see cref="IMember"/> for all members with the specified email.
/// </summary>
//// <param name="email">Email to use for retrieval</param>
/// <returns>
/// <see cref="IEnumerable{IMember}" />
/// </returns>
IEnumerable<IMember> GetMembersByEmail(string email)
=>
// TODO (V16): Remove this default implementation.
// The following is very inefficient, but will return the correct data, so probably better than throwing a NotImplementedException
// in the default implentation here, for, presumably rare, cases where a custom IMemberService implementation has been registered and
// does not override this method.
GetAllMembers().Where(x => x.Email.Equals(email));
/// <summary>
/// Gets all Members for the specified MemberType alias
/// </summary>
@@ -328,4 +343,11 @@ public interface IMemberService : IMembershipMemberService
/// <see cref="IEnumerable{IMember}" />
/// </returns>
IEnumerable<IMember>? GetMembersByPropertyValue(string propertyTypeAlias, DateTime value, ValuePropertyMatchType matchType = ValuePropertyMatchType.Exact);
/// <summary>
/// Saves only the properties related to login for the member, using an optimized, non-locking update.
/// </summary>
/// <param name="member">The member to update.</param>
/// <returns>Used to avoid the full save of the member object after a login operation.</returns>
Task UpdateLoginPropertiesAsync(IMember member) => Task.CompletedTask;
}
@@ -135,6 +135,14 @@ public interface IMembershipMemberService<T> : IService
/// <param name="entity"><see cref="IMember" /> or <see cref="IUser" /> to Save</param>
void Save(T entity);
/// <summary>
/// Saves an <see cref="IMembershipUser" />
/// </summary>
/// <remarks>An <see cref="IMembershipUser" /> can be of type <see cref="IMember" /> or <see cref="IUser" /></remarks>
/// <param name="entity"><see cref="IMember" /> or <see cref="IUser" /> to Save</param>
/// <param name="publishNotificationSaveOptions"> Enum for deciding which notifications to publish.</param>
void Save(T entity, PublishNotificationSaveOptions publishNotificationSaveOptions) => Save(entity);
/// <summary>
/// Saves a list of <see cref="IMembershipUser" /> objects
/// </summary>
+21
View File
@@ -231,8 +231,29 @@ public interface IUserService : IMembershipUserService
/// </returns>
IEnumerable<IUser> GetAllNotInGroup(int groupId);
/// <summary>
/// Gets a page of users, ordered by Id and starting from the provided Id.
/// </summary>
/// <param name="id">The user Id to start retrieving users from.</param>
/// <param name="count">The number of users to return.</param>
/// <returns>A page of <see cref="IUser"/> instances.</returns>
[Obsolete("No longer used in Umbraco. Scheduled for removal in Umbraco 18.")]
IEnumerable<IUser> GetNextUsers(int id, int count);
/// <summary>
/// Gets a page of approved users, ordered by Id and starting from the provided Id.
/// </summary>
/// <param name="id">The user Id to start retrieving users from.</param>
/// <param name="count">The number of users to return.</param>
/// <returns>A page of <see cref="IUser"/> instances.</returns>
IEnumerable<IUser> GetNextApprovedUsers(int id, int count) => Enumerable.Empty<IUser>();
/// <summary>
/// Invalidates sessions for users that aren't associated with the current collection of providers.
/// </summary>
/// <param name="currentLoginProviders">The keys for the currently configured providers.</param>
void InvalidateSessionsForRemovedProviders(IEnumerable<string> currentLoginProviders) { }
#region User groups
/// <summary>
+2 -2
View File
@@ -418,7 +418,7 @@ namespace Umbraco.Cms.Core.Services
}
using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true);
scope.ReadLock(Constants.Locks.ContentTree);
scope.ReadLock(Constants.Locks.MediaTree);
return _mediaRepository.GetPage(Query<IMedia>()?.Where(x => x.ContentTypeId == contentTypeId), pageIndex, pageSize, out totalRecords, filter, ordering);
}
@@ -441,7 +441,7 @@ namespace Umbraco.Cms.Core.Services
}
using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true);
scope.ReadLock(Constants.Locks.ContentTree);
scope.ReadLock(Constants.Locks.MediaTree);
return _mediaRepository.GetPage(
Query<IMedia>()?.Where(x => contentTypeIds.Contains(x.ContentTypeId)), pageIndex, pageSize, out totalRecords, filter, ordering);
}
+70 -9
View File
@@ -389,16 +389,23 @@ namespace Umbraco.Cms.Core.Services
}
/// <summary>
/// Get an <see cref="IMember"/> by email
/// Get an <see cref="IMember"/> by email. If RequireUniqueEmailForMembers is set to false, then the first member found with the specified email will be returned.
/// </summary>
/// <param name="email">Email to use for retrieval</param>
/// <returns><see cref="IMember"/></returns>
public IMember? GetByEmail(string email)
public IMember? GetByEmail(string email) => GetMembersByEmail(email).FirstOrDefault();
/// <summary>
/// Get an list of <see cref="IMember"/> for all members with the specified email.
/// </summary>
/// <param name="email">Email to use for retrieval</param>
/// <returns><see cref="IEnumerable{IMember}"/></returns>
public IEnumerable<IMember> GetMembersByEmail(string email)
{
using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true);
scope.ReadLock(Constants.Locks.MemberTree);
IQuery<IMember> query = Query<IMember>().Where(x => x.Email.Equals(email));
return _memberRepository.Get(query)?.FirstOrDefault();
return _memberRepository.Get(query);
}
/// <summary>
@@ -736,7 +743,9 @@ namespace Umbraco.Cms.Core.Services
public void SetLastLogin(string username, DateTime date) => throw new NotImplementedException();
/// <inheritdoc />
public void Save(IMember member)
public void Save(IMember member) => Save(member, PublishNotificationSaveOptions.All);
public void Save(IMember member, PublishNotificationSaveOptions publishNotificationSaveOptions)
{
// trimming username and email to make sure we have no trailing space
member.Username = member.Username.Trim();
@@ -745,11 +754,15 @@ namespace Umbraco.Cms.Core.Services
EventMessages evtMsgs = EventMessagesFactory.Get();
using ICoreScope scope = ScopeProvider.CreateCoreScope();
var savingNotification = new MemberSavingNotification(member, evtMsgs);
if (scope.Notifications.PublishCancelable(savingNotification))
MemberSavingNotification? savingNotification = null;
if (publishNotificationSaveOptions.HasFlag(PublishNotificationSaveOptions.Saving))
{
scope.Complete();
return;
savingNotification = new MemberSavingNotification(member, evtMsgs);
if (scope.Notifications.PublishCancelable(savingNotification))
{
scope.Complete();
return;
}
}
if (string.IsNullOrWhiteSpace(member.Name))
@@ -761,7 +774,13 @@ namespace Umbraco.Cms.Core.Services
_memberRepository.Save(member);
scope.Notifications.Publish(new MemberSavedNotification(member, evtMsgs).WithStateFrom(savingNotification));
if (publishNotificationSaveOptions.HasFlag(PublishNotificationSaveOptions.Saved))
{
scope.Notifications.Publish(
savingNotification is null
? new MemberSavedNotification(member, evtMsgs)
: new MemberSavedNotification(member, evtMsgs).WithStateFrom(savingNotification));
}
Audit(AuditType.Save, 0, member.Id);
@@ -801,6 +820,48 @@ namespace Umbraco.Cms.Core.Services
scope.Complete();
}
/// <inheritdoc/>
/// <remarks>
/// <para>
/// Note that in this optimized member save operation for use in the login process, where we only handle login related
/// properties, we aren't taking any locks. If we were updating "content" properties, that could have relations between each
/// other, we should following what we do for documents and lock.
/// But here we are just updating these system fields, and it's fine if they work in a "last one wins" fashion without locking.
/// </para>
/// <para>
/// Note also that we aren't calling "Audit" here (as well as to optimize performance, this is deliberate, because this is not
/// a full save operation on the member that we'd want to audit who made the changes via the backoffice or API; rather it's
/// just the member logging in as themselves).
/// </para>
/// <para>
/// We are though publishing notifications, to maintain backwards compatibility for any solutions using these for
/// processing following a member login.
/// </para>
/// <para>
/// These notification handlers will ensure that the records to umbracoLog are also added in the same way as they
/// are for a full save operation.
/// </para>
/// </remarks>
public async Task UpdateLoginPropertiesAsync(IMember member)
{
EventMessages evtMsgs = EventMessagesFactory.Get();
using ICoreScope scope = ScopeProvider.CreateCoreScope();
var savingNotification = new MemberSavingNotification(member, evtMsgs);
savingNotification.State.Add("LoginPropertiesOnly", true);
if (scope.Notifications.PublishCancelable(savingNotification))
{
scope.Complete();
return;
}
await _memberRepository.UpdateLoginPropertiesAsync(member);
scope.Notifications.Publish(new MemberSavedNotification(member, evtMsgs).WithStateFrom(savingNotification));
scope.Complete();
}
#endregion
#region Delete
@@ -96,7 +96,7 @@ public class NotificationService : INotificationService
// see notes above
var id = Constants.Security.SuperUserId;
const int pagesz = 400; // load batches of 400 users
const int UserBatchSize = 400; // load batches of 400 users
do
{
var notifications = GetUsersNotifications(new List<int>(), action, Enumerable.Empty<int>(), Constants.ObjectTypes.Document)?.ToList();
@@ -106,10 +106,10 @@ public class NotificationService : INotificationService
}
// users are returned ordered by id, notifications are returned ordered by user id
var users = _userService.GetNextUsers(id, pagesz).Where(x => x.IsApproved).ToList();
foreach (IUser user in users)
var approvedUsers = _userService.GetNextApprovedUsers(id, UserBatchSize).ToList();
foreach (IUser approvedUser in approvedUsers)
{
Notification[] userNotifications = notifications.Where(n => n.UserId == user.Id).ToArray();
Notification[] userNotifications = notifications.Where(n => n.UserId == approvedUser.Id).ToArray();
foreach (Notification notification in userNotifications)
{
// notifications are inherited down the tree - find the topmost entity
@@ -130,14 +130,14 @@ public class NotificationService : INotificationService
}
// queue notification
NotificationRequest req = CreateNotificationRequest(operatingUser, user, entityForNotification, prevVersionDictionary[entityForNotification.Id], actionName, siteUri, createSubject, createBody);
NotificationRequest req = CreateNotificationRequest(operatingUser, approvedUser, entityForNotification, prevVersionDictionary[entityForNotification.Id], actionName, siteUri, createSubject, createBody);
Enqueue(req);
break;
}
}
// load more users if any
id = users.Count == pagesz ? users.Last().Id + 1 : -1;
id = approvedUsers.Count == UserBatchSize ? approvedUsers.Last().Id + 1 : -1;
}
while (id > 0);
}
+20
View File
@@ -712,6 +712,7 @@ internal class UserService : RepositoryService, IUserService
}
}
/// <inheritdoc/>
public IEnumerable<IUser> GetNextUsers(int id, int count)
{
using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
@@ -720,6 +721,25 @@ internal class UserService : RepositoryService, IUserService
}
}
/// <inheritdoc/>
public IEnumerable<IUser> GetNextApprovedUsers(int id, int count)
{
using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
{
return _userRepository.GetNextApprovedUsers(id, count);
}
}
/// <inheritdoc />
public void InvalidateSessionsForRemovedProviders(IEnumerable<string> currentLoginProviders)
{
using (ICoreScope scope = ScopeProvider.CreateCoreScope())
{
_userRepository.InvalidateSessionsForRemovedProviders(currentLoginProviders);
scope.Complete();
}
}
/// <summary>
/// Gets a list of <see cref="IUser" /> objects associated with a given group
/// </summary>
@@ -74,12 +74,21 @@ public class DefaultShortStringHelperConfig
{
urlSegmentConvertTo = CleanStringType.Ascii;
}
if (requestHandlerSettings.ShouldTryConvertUrlsToAscii)
else if (requestHandlerSettings.ShouldTryConvertUrlsToAscii)
{
urlSegmentConvertTo = CleanStringType.TryAscii;
}
CleanStringType fileNameSegmentConvertTo = CleanStringType.Utf8;
if (requestHandlerSettings.ShouldConvertFileNamesToAscii)
{
fileNameSegmentConvertTo = CleanStringType.Ascii;
}
else if (requestHandlerSettings.ShouldTryConvertFileNamesToAscii)
{
fileNameSegmentConvertTo = CleanStringType.TryAscii;
}
return WithConfig(CleanStringType.UrlSegment, new Config
{
PreFilter = ApplyUrlReplaceCharacters,
@@ -92,7 +101,7 @@ public class DefaultShortStringHelperConfig
{
PreFilter = ApplyUrlReplaceCharacters,
IsTerm = (c, leading) => char.IsLetterOrDigit(c) || c == '_', // letter, digit or underscore
StringType = CleanStringType.Utf8 | CleanStringType.LowerCase,
StringType = fileNameSegmentConvertTo | CleanStringType.LowerCase,
BreakTermsOnUpper = false,
Separator = '-',
}).WithConfig(CleanStringType.Alias, new Config
+166
View File
@@ -0,0 +1,166 @@
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 TimeProvider _timeProvider;
private readonly CancellationTokenSource _cancellationTokenSource;
private readonly long _startingTimestamp;
/// <summary>
/// Gets the elapsed time.
/// </summary>
/// <value>
/// The elapsed time.
/// </value>
public TimeSpan Elapsed
=> _timeProvider.GetElapsedTime(_startingTimestamp);
/// <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="millisecondsDuration">The number of milliseconds the scope should at least take.</param>
/// <param name="timeProvider">The time provider.</param>
public TimedScope(long millisecondsDuration, TimeProvider timeProvider)
: this(TimeSpan.FromMilliseconds(millisecondsDuration), timeProvider)
{ }
/// <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="timeProvider">The time provider.</param>
/// <param name="cancellationToken">The cancellation token.</param>
public TimedScope(long millisecondsDuration, TimeProvider timeProvider, CancellationToken cancellationToken)
: this(TimeSpan.FromMilliseconds(millisecondsDuration), timeProvider, 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, TimeProvider.System)
{ }
/// <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="timeProvider">The time provider.</param>
public TimedScope(TimeSpan duration, TimeProvider timeProvider)
: this(duration, timeProvider, 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, TimeProvider.System, 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>
/// <param name="timeProvider">The time provider.</param>
/// <param name="cancellationToken">The cancellation token.</param>
public TimedScope(TimeSpan duration, TimeProvider timeProvider, CancellationToken cancellationToken)
: this(duration, timeProvider, CancellationTokenSource.CreateLinkedTokenSource(cancellationToken))
{ }
private TimedScope(TimeSpan duration, TimeProvider timeProvider, CancellationTokenSource cancellationTokenSource)
{
_duration = duration;
_timeProvider = timeProvider;
_cancellationTokenSource = cancellationTokenSource;
_startingTimestamp = timeProvider.GetTimestamp();
}
/// <summary>
/// Cancels the timed scope.
/// </summary>
public void Cancel()
=> _cancellationTokenSource.Cancel();
/// <summary>
/// Cancels the timed scope asynchronously.
/// </summary>
public async Task CancelAsync()
=> await _cancellationTokenSource.CancelAsync().ConfigureAwait(false);
/// <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);
}
_cancellationTokenSource.Dispose();
}
/// <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, TimeProvider, 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, _timeProvider, _cancellationTokenSource.Token).ConfigureAwait(false);
}
_cancellationTokenSource.Dispose();
}
private bool TryGetRemaining(out TimeSpan remaining)
{
remaining = _duration.Subtract(Elapsed);
return remaining > TimeSpan.Zero;
}
}
@@ -9,6 +9,15 @@ namespace Umbraco.Cms.Core.DependencyInjection;
/// </summary>
public static class WebhookEventCollectionBuilderCmsExtensions
{
private static readonly Type[] _defaultTypes =
[
typeof(ContentDeletedWebhookEvent),
typeof(ContentPublishedWebhookEvent),
typeof(ContentUnpublishedWebhookEvent),
typeof(MediaDeletedWebhookEvent),
typeof(MediaSavedWebhookEvent),
];
/// <summary>
/// Adds the default webhook events.
/// </summary>
@@ -21,12 +30,24 @@ public static class WebhookEventCollectionBuilderCmsExtensions
/// </remarks>
public static WebhookEventCollectionBuilderCms AddDefault(this WebhookEventCollectionBuilderCms builder)
{
builder.Builder
.Add<ContentDeletedWebhookEvent>()
.Add<ContentPublishedWebhookEvent>()
.Add<ContentUnpublishedWebhookEvent>()
.Add<MediaDeletedWebhookEvent>()
.Add<MediaSavedWebhookEvent>();
builder.Builder.Add(_defaultTypes);
return builder;
}
/// <summary>
/// Removes the default webhook events.
/// </summary>
/// <param name="builder">The builder.</param>
/// <returns>
/// The builder.
/// </returns>
public static WebhookEventCollectionBuilderCms RemoveDefault(this WebhookEventCollectionBuilderCms builder)
{
foreach (Type type in _defaultTypes)
{
builder.Builder.Remove(type);
}
return builder;
}
@@ -10,6 +10,8 @@
<PackageReference Include="Examine" />
<!-- Take top-level depedendency on System.Security.Cryptography.Xml, because Examine depends on a vulnerable version -->
<PackageReference Include="System.Security.Cryptography.Xml" />
<!-- Take top-level depedendency on Lucene.Net.Replicator-->
<PackageReference Include="Lucene.Net.Replicator" />
</ItemGroup>
<ItemGroup>
@@ -51,6 +51,12 @@ public class WebhookFiring : IRecurringBackgroundJob
public async Task RunJobAsync()
{
if (_webhookSettings.Enabled is false)
{
_logger.LogInformation("WebhookFiring task will not run as it has been globally disabled via configuration");
return;
}
IEnumerable<WebhookRequest> requests;
using (ICoreScope scope = _coreScopeProvider.CreateCoreScope())
{
@@ -116,6 +116,7 @@ public class DefaultRepositoryCachePolicy<TEntity, TId> : RepositoryCachePolicyB
{
// whatever happens, clear the cache
var cacheKey = GetEntityCacheKey(entity.Id);
Cache.Clear(cacheKey);
// if there's a GetAllCacheAllowZeroCount cache, ensure it is cleared
@@ -127,20 +128,34 @@ public class DefaultRepositoryCachePolicy<TEntity, TId> : RepositoryCachePolicyB
public override TEntity? Get(TId? id, Func<TId?, TEntity?> performGet, Func<TId[]?, IEnumerable<TEntity>?> performGetAll)
{
var cacheKey = GetEntityCacheKey(id);
TEntity? fromCache = Cache.GetCacheItem<TEntity>(cacheKey);
// if found in cache then return else fetch and cache
if (fromCache != null)
// If found in cache then return immediately.
if (fromCache is not null)
{
return fromCache;
}
// If we've cached a "null" value, return null.
if (_options.CacheNullValues && Cache.GetCacheItem<string>(cacheKey) == Constants.Cache.NullRepresentationInCache)
{
return null;
}
// Otherwise go to the database to retrieve.
TEntity? entity = performGet(id);
if (entity != null && entity.HasIdentity)
{
// If we've found an identified entity, cache it for subsequent retrieval.
InsertEntity(cacheKey, entity);
}
else if (entity is null && _options.CacheNullValues)
{
// If we've not found an entity, and we're caching null values, cache a "null" value.
InsertNull(cacheKey);
}
return entity;
}
@@ -248,6 +263,15 @@ public class DefaultRepositoryCachePolicy<TEntity, TId> : RepositoryCachePolicyB
protected virtual void InsertEntity(string cacheKey, TEntity entity)
=> Cache.Insert(cacheKey, () => entity, TimeSpan.FromMinutes(5), true);
protected virtual void InsertNull(string cacheKey)
{
// We can't actually cache a null value, as in doing so wouldn't be able to distinguish between
// a value that does exist but isn't yet cached, or a value that has been explicitly cached with a null value.
// Both would return null when we retrieve from the cache and we couldn't distinguish between the two.
// So we cache a special value that represents null, and then we can check for that value when we retrieve from the cache.
Cache.Insert(cacheKey, () => Constants.Cache.NullRepresentationInCache, TimeSpan.FromMinutes(5), true);
}
protected virtual void InsertEntities(TId[]? ids, TEntity[]? entities)
{
if (ids?.Length == 0 && entities?.Length == 0 && _options.GetAllCacheAllowZeroCount)
@@ -0,0 +1,39 @@
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Infrastructure.Scoping;
using Umbraco.Extensions;
namespace Umbraco.Cms.Core.Cache;
public class MemberRepositoryUsernameCachePolicy : DefaultRepositoryCachePolicy<IMember, string>
{
public MemberRepositoryUsernameCachePolicy(IAppPolicyCache cache, IScopeAccessor scopeAccessor, RepositoryCachePolicyOptions options) : base(cache, scopeAccessor, options)
{
}
public IMember? GetByUserName(string key, string? username, Func<string?, IMember?> performGetByUsername, Func<string[]?, IEnumerable<IMember>?> performGetAll)
{
var cacheKey = GetEntityCacheKey(key + username);
IMember? fromCache = Cache.GetCacheItem<IMember>(cacheKey);
// if found in cache then return else fetch and cache
if (fromCache != null)
{
return fromCache;
}
IMember? entity = performGetByUsername(username);
if (entity != null && entity.HasIdentity)
{
InsertEntity(cacheKey, entity);
}
return entity;
}
public void DeleteByUserName(string key, string? username)
{
var cacheKey = GetEntityCacheKey(key + username);
Cache.ClearByKey(cacheKey);
}
}
@@ -1,13 +1,10 @@
using HtmlAgilityPack;
using Microsoft.Extensions.DependencyInjection;
using HtmlAgilityPack;
using Microsoft.Extensions.Logging;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.DeliveryApi;
using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Core.Models.Blocks;
using Umbraco.Cms.Core.Models.DeliveryApi;
using Umbraco.Cms.Core.PublishedCache;
using Umbraco.Cms.Core.Routing;
using Umbraco.Cms.Infrastructure.Extensions;
using Umbraco.Extensions;
@@ -101,8 +98,9 @@ internal sealed class ApiRichTextElementParser : ApiRichTextParserBase, IApiRich
// - non-#comment nodes
// - non-#text nodes
// - non-empty #text nodes
// - empty #text between inline elements (see #17037) but not #text with only newlines (see #19388)
HtmlNode[] childNodes = element.ChildNodes
.Where(c => c.Name != CommentNodeName && (c.Name != TextNodeName || string.IsNullOrWhiteSpace(c.InnerText) is false))
.Where(c => c.Name != CommentNodeName && (c.Name != TextNodeName || IsNonEmptyElement(c)))
.ToArray();
var tag = TagName(element);
@@ -123,6 +121,9 @@ internal sealed class ApiRichTextElementParser : ApiRichTextParserBase, IApiRich
return createElement(tag, attributes, childElements);
}
private static bool IsNonEmptyElement(HtmlNode htmlNode) =>
string.IsNullOrWhiteSpace(htmlNode.InnerText) is false || htmlNode.InnerText.Any(c => c != '\n' && c != '\r');
private string TagName(HtmlNode htmlNode) => htmlNode.Name;
private void ReplaceLocalLinks(IPublishedSnapshot publishedSnapshot, Dictionary<string, object> attributes)
@@ -240,6 +240,7 @@ public static partial class UmbracoBuilderExtensions
builder.Services.AddSingleton<IBlockEditorElementTypeCache, BlockEditorElementTypeCache>();
builder.Services.AddSingleton<IRichTextRequiredValidator, RichTextRequiredValidator>();
builder.Services.AddSingleton<IRichTextRegexValidator, RichTextRegexValidator>();
return builder;
}
@@ -454,6 +455,7 @@ public static partial class UmbracoBuilderExtensions
builder.Services.AddSingleton<IRequestRedirectService, NoopRequestRedirectService>();
builder.Services.AddSingleton<IRequestPreviewService, NoopRequestPreviewService>();
builder.Services.AddSingleton<IRequestMemberAccessService, NoopRequestMemberAccessService>();
builder.Services.AddTransient<ICurrentMemberClaimsProvider, NoopCurrentMemberClaimsProvider>();
builder.Services.AddSingleton<IApiAccessService, NoopApiAccessService>();
builder.Services.AddSingleton<IApiContentQueryService, NoopApiContentQueryService>();
builder.Services.AddSingleton<IApiMediaQueryService, NoopApiMediaQueryService>();
@@ -1,4 +1,4 @@
using Microsoft.Extensions.Options;
using Microsoft.Extensions.Options;
using Umbraco.Cms.Core.Configuration.Models;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Persistence.Querying;
@@ -28,21 +28,28 @@ internal sealed class DeliveryApiContentIndexHelper : IDeliveryApiContentIndexHe
public void EnumerateApplicableDescendantsForContentIndex(int rootContentId, Action<IContent[]> actionToPerform)
{
const int pageSize = 10000;
var pageIndex = 0;
EnumerateApplicableDescendantsForContentIndex(rootContentId, actionToPerform, pageSize);
}
internal void EnumerateApplicableDescendantsForContentIndex(int rootContentId, Action<IContent[]> actionToPerform, int pageSize)
{
var itemIndex = 0;
long total;
IQuery<IContent> query = _umbracoDatabaseFactory.SqlContext.Query<IContent>().Where(content => content.Trashed == false);
IContent[] descendants;
IQuery<IContent> query = _umbracoDatabaseFactory.SqlContext.Query<IContent>().Where(content => content.Trashed == false);
do
{
descendants = _contentService
.GetPagedDescendants(rootContentId, pageIndex, pageSize, out _, query, Ordering.By("Path"))
.GetPagedDescendants(rootContentId, itemIndex / pageSize, pageSize, out total, query, Ordering.By("Path"))
.Where(descendant => _deliveryApiSettings.IsAllowedContentType(descendant.ContentType.Alias))
.ToArray();
actionToPerform(descendants.ToArray());
actionToPerform(descendants);
pageIndex++;
itemIndex += pageSize;
}
while (descendants.Length == pageSize);
while (descendants.Length > 0 && itemIndex < total);
}
}
@@ -187,8 +187,7 @@ public class ExamineIndexRebuilder : IIndexRebuilder
{
// If an index exists but it has zero docs we'll consider it empty and rebuild
IIndex[] indexes = (onlyEmptyIndexes
? _examineManager.Indexes.Where(x =>
!x.IndexExists() || (x is IIndexStats stats && stats.GetDocumentCount() == 0))
? _examineManager.Indexes.Where(ShouldRebuild)
: _examineManager.Indexes).ToArray();
if (indexes.Length == 0)
@@ -228,4 +227,17 @@ public class ExamineIndexRebuilder : IIndexRebuilder
}
}
}
private bool ShouldRebuild(IIndex index)
{
try
{
return !index.IndexExists() || (index is IIndexStats stats && stats.GetDocumentCount() == 0);
}
catch (Exception e)
{
_logger.LogError(e, "An error occured trying to get determine index shouldRebuild status for index {IndexName}. The index will NOT be considered for rebuilding", index.Name);
return false;
}
}
}
@@ -1,5 +1,9 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Configuration;
using Umbraco.Cms.Core.Configuration.Models;
using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Core.Events;
using Umbraco.Cms.Core.Exceptions;
using Umbraco.Cms.Core.Logging;
@@ -23,19 +27,39 @@ public class UnattendedUpgrader : INotificationAsyncHandler<RuntimeUnattendedUpg
private readonly IProfilingLogger _profilingLogger;
private readonly IRuntimeState _runtimeState;
private readonly IUmbracoVersion _umbracoVersion;
private readonly UnattendedSettings _unattendedSettings;
public UnattendedUpgrader(
IProfilingLogger profilingLogger,
IUmbracoVersion umbracoVersion,
DatabaseBuilder databaseBuilder,
IRuntimeState runtimeState,
PackageMigrationRunner packageMigrationRunner)
PackageMigrationRunner packageMigrationRunner,
IOptions<UnattendedSettings> unattendedSettings)
{
_profilingLogger = profilingLogger ?? throw new ArgumentNullException(nameof(profilingLogger));
_umbracoVersion = umbracoVersion ?? throw new ArgumentNullException(nameof(umbracoVersion));
_databaseBuilder = databaseBuilder ?? throw new ArgumentNullException(nameof(databaseBuilder));
_runtimeState = runtimeState ?? throw new ArgumentNullException(nameof(runtimeState));
_packageMigrationRunner = packageMigrationRunner;
_unattendedSettings = unattendedSettings.Value;
}
[Obsolete("Use constructor that takes IOptions<UnattendedSettings>, this will be removed in V16")]
public UnattendedUpgrader(
IProfilingLogger profilingLogger,
IUmbracoVersion umbracoVersion,
DatabaseBuilder databaseBuilder,
IRuntimeState runtimeState,
PackageMigrationRunner packageMigrationRunner)
: this(
profilingLogger,
umbracoVersion,
databaseBuilder,
runtimeState,
packageMigrationRunner,
StaticServiceProvider.Instance.GetRequiredService<IOptions<UnattendedSettings>>())
{
}
public Task HandleAsync(RuntimeUnattendedUpgradeNotification notification, CancellationToken cancellationToken)
@@ -46,55 +70,26 @@ public class UnattendedUpgrader : INotificationAsyncHandler<RuntimeUnattendedUpg
{
case RuntimeLevelReason.UpgradeMigrations:
{
var plan = new UmbracoPlan(_umbracoVersion);
using (!_profilingLogger.IsEnabled(Core.Logging.LogLevel.Verbose) ? null : _profilingLogger.TraceDuration<UnattendedUpgrader>(
"Starting unattended upgrade.",
"Unattended upgrade completed."))
{
DatabaseBuilder.Result? result = _databaseBuilder.UpgradeSchemaAndData(plan);
if (result?.Success == false)
{
var innerException = new UnattendedInstallException(
"An error occurred while running the unattended upgrade.\n" + result.Message);
_runtimeState.Configure(RuntimeLevel.BootFailed, RuntimeLevelReason.BootFailedOnException, innerException);
}
RunUpgrade(notification);
notification.UnattendedUpgradeResult =
RuntimeUnattendedUpgradeNotification.UpgradeResult.CoreUpgradeComplete;
// If we errored out when upgrading don't do anything.
if (notification.UnattendedUpgradeResult is RuntimeUnattendedUpgradeNotification.UpgradeResult.HasErrors)
{
return Task.CompletedTask;
}
// It's entirely possible that there's both a core upgrade and package migrations to run, so try and run package migrations too.
// but only if upgrade unattended is enabled.
if (_unattendedSettings.PackageMigrationsUnattended)
{
RunPackageMigrations(notification);
}
}
break;
case RuntimeLevelReason.UpgradePackageMigrations:
{
if (!_runtimeState.StartupState.TryGetValue(
RuntimeState.PendingPackageMigrationsStateKey,
out var pm)
|| pm is not IReadOnlyList<string> pendingMigrations)
{
throw new InvalidOperationException(
$"The required key {RuntimeState.PendingPackageMigrationsStateKey} does not exist in startup state");
}
if (pendingMigrations.Count == 0)
{
throw new InvalidOperationException(
"No pending migrations found but the runtime level reason is " +
RuntimeLevelReason.UpgradePackageMigrations);
}
try
{
_packageMigrationRunner.RunPackagePlans(pendingMigrations);
notification.UnattendedUpgradeResult = RuntimeUnattendedUpgradeNotification.UpgradeResult
.PackageMigrationComplete;
}
catch (Exception ex)
{
SetRuntimeError(ex);
notification.UnattendedUpgradeResult =
RuntimeUnattendedUpgradeNotification.UpgradeResult.HasErrors;
}
RunPackageMigrations(notification);
}
break;
@@ -106,6 +101,64 @@ public class UnattendedUpgrader : INotificationAsyncHandler<RuntimeUnattendedUpg
return Task.CompletedTask;
}
private void RunPackageMigrations(RuntimeUnattendedUpgradeNotification notification)
{
if (_runtimeState.StartupState.TryGetValue(
RuntimeState.PendingPackageMigrationsStateKey,
out var pm) is false
|| pm is not IReadOnlyList<string> pendingMigrations)
{
throw new InvalidOperationException(
$"The required key {RuntimeState.PendingPackageMigrationsStateKey} does not exist in startup state");
}
if (pendingMigrations.Count == 0)
{
// If we determined we needed to run package migrations but there are none, this is an error
if (_runtimeState.Reason is RuntimeLevelReason.UpgradePackageMigrations)
{
throw new InvalidOperationException(
"No pending migrations found but the runtime level reason is " +
RuntimeLevelReason.UpgradePackageMigrations);
}
return;
}
try
{
_packageMigrationRunner.RunPackagePlans(pendingMigrations);
notification.UnattendedUpgradeResult = RuntimeUnattendedUpgradeNotification.UpgradeResult
.PackageMigrationComplete;
}
catch (Exception ex)
{
SetRuntimeError(ex);
notification.UnattendedUpgradeResult =
RuntimeUnattendedUpgradeNotification.UpgradeResult.HasErrors;
}
}
private void RunUpgrade(RuntimeUnattendedUpgradeNotification notification)
{
var plan = new UmbracoPlan(_umbracoVersion);
using (!_profilingLogger.IsEnabled(Core.Logging.LogLevel.Verbose) ? null : _profilingLogger.TraceDuration<UnattendedUpgrader>(
"Starting unattended upgrade.",
"Unattended upgrade completed."))
{
DatabaseBuilder.Result? result = _databaseBuilder.UpgradeSchemaAndData(plan);
if (result?.Success == false)
{
var innerException = new UnattendedInstallException(
"An error occurred while running the unattended upgrade.\n" + result.Message);
_runtimeState.Configure(RuntimeLevel.BootFailed, RuntimeLevelReason.BootFailedOnException, innerException);
}
notification.UnattendedUpgradeResult =
RuntimeUnattendedUpgradeNotification.UpgradeResult.CoreUpgradeComplete;
}
}
private void SetRuntimeError(Exception exception)
=> _runtimeState.Configure(
RuntimeLevel.BootFailed,
@@ -70,6 +70,11 @@ public abstract class MigrationExpressionBase : IMigrationExpression
}
else
{
if (stmtBuilder.Length > 0)
{
stmtBuilder.Append(Environment.NewLine);
}
stmtBuilder.Append(line);
}
}
@@ -92,7 +92,6 @@ public class MigrationPlanExecutor : IMigrationPlanExecutor
/// <para>Each migration in the plan, may or may not run in a scope depending on the type of plan.</para>
/// <para>A plan can complete partially, the changes of each completed migration will be saved.</para>
/// </remarks>
[Obsolete("This will return an ExecutedMigrationPlan in V13")]
public ExecutedMigrationPlan ExecutePlan(MigrationPlan plan, string fromState)
{
plan.Validate();
@@ -1,4 +1,4 @@
using NPoco;
using NPoco;
using Umbraco.Cms.Infrastructure.Persistence;
using Umbraco.Cms.Infrastructure.Persistence.Dtos;
using ColumnInfo = Umbraco.Cms.Infrastructure.Persistence.SqlSyntax.ColumnInfo;
@@ -153,16 +153,26 @@ SELECT obj_Constraint.NAME AS 'constraintName'
");
var currentConstraintName = Database.ExecuteScalar<string>(constraintNameQuery);
// only rename the constraint if necessary
// Only rename the constraint if necessary.
if (currentConstraintName == expectedConstraintName)
{
return;
}
Sql<ISqlContext> renameConstraintQuery = Database.SqlContext.Sql(
$"EXEC sp_rename N'{currentConstraintName}', N'{expectedConstraintName}', N'OBJECT'");
Database.Execute(renameConstraintQuery);
if (currentConstraintName is null)
{
// Constraint does not exist, so we need to create it.
Sql<ISqlContext> createConstraintStatement = Database.SqlContext.Sql(@$"
ALTER TABLE umbracoContentVersion ADD CONSTRAINT [DF_umbracoContentVersion_versionDate] DEFAULT (getdate()) FOR [versionDate]");
Database.Execute(createConstraintStatement);
}
else
{
// Constraint exists, and differs from the expected name, so we need to rename it.
Sql<ISqlContext> renameConstraintQuery = Database.SqlContext.Sql(
$"EXEC sp_rename N'{currentConstraintName}', N'{expectedConstraintName}', N'OBJECT'");
Database.Execute(renameConstraintQuery);
}
}
private void UpdateExternalLoginIndexes(IEnumerable<Tuple<string, string, string, bool>> indexes)
@@ -51,7 +51,15 @@ public class TextBuilder : Builder
/// Outputs an "auto-generated" header to a string builder.
/// </summary>
/// <param name="sb">The string builder.</param>
public static void WriteHeader(StringBuilder sb) => TextHeaderWriter.WriteHeader(sb);
[Obsolete("Please use the overload taking all parameters. Scheduled for removal in Umbraco 17.")]
public static void WriteHeader(StringBuilder sb) => WriteHeader(sb, true);
/// <summary>
/// Outputs an "auto-generated" header to a string builder.
/// </summary>
/// <param name="sb">The string builder.</param>
/// <param name="includeVersion">Flag indicating whether the tool version number should be included in the output.</param>
public static void WriteHeader(StringBuilder sb, bool includeVersion) => TextHeaderWriter.WriteHeader(sb, includeVersion);
/// <summary>
/// Outputs a generated model to a string builder.
@@ -60,7 +68,7 @@ public class TextBuilder : Builder
/// <param name="typeModel">The model to generate.</param>
public void Generate(StringBuilder sb, TypeModel typeModel)
{
WriteHeader(sb);
WriteHeader(sb, Config.IncludeVersionNumberInGeneratedModels);
foreach (var t in TypesUsing)
{
@@ -83,7 +91,7 @@ public class TextBuilder : Builder
/// <param name="typeModels">The models to generate.</param>
public void Generate(StringBuilder sb, IEnumerable<TypeModel> typeModels)
{
WriteHeader(sb);
WriteHeader(sb, Config.IncludeVersionNumberInGeneratedModels);
foreach (var t in TypesUsing)
{
@@ -143,14 +151,17 @@ public class TextBuilder : Builder
//
// note that the blog post above clearly states that "Nor should it be applied at the type level if the type being generated is a partial class."
// and since our models are partial classes, we have to apply the attribute against the individual members, not the class itself.
private static void WriteGeneratedCodeAttribute(StringBuilder sb, string tabs) => sb.AppendFormat(
private void WriteGeneratedCodeAttribute(StringBuilder sb, string tabs) => sb.AppendFormat(
"{0}[global::System.CodeDom.Compiler.GeneratedCodeAttribute(\"Umbraco.ModelsBuilder.Embedded\", \"{1}\")]\n",
tabs, ApiVersion.Current.Version);
tabs,
Config.IncludeVersionNumberInGeneratedModels ? ApiVersion.Current.Version : null);
// writes an attribute that specifies that an output may be null.
// (useful for consuming projects with nullable reference types enabled)
private static void WriteMaybeNullAttribute(StringBuilder sb, string tabs, bool isReturn = false) =>
sb.AppendFormat("{0}[{1}global::System.Diagnostics.CodeAnalysis.MaybeNull]\n", tabs,
sb.AppendFormat(
"{0}[{1}global::System.Diagnostics.CodeAnalysis.MaybeNull]\n",
tabs,
isReturn ? "return: " : string.Empty);
private static string MixinStaticGetterName(string clrName) => string.Format("Get{0}", clrName);
@@ -8,13 +8,30 @@ internal static class TextHeaderWriter
/// Outputs an "auto-generated" header to a string builder.
/// </summary>
/// <param name="sb">The string builder.</param>
public static void WriteHeader(StringBuilder sb)
[Obsolete("Please use the overload taking all parameters. Scheduled for removal in Umbraco 17.")]
public static void WriteHeader(StringBuilder sb) => WriteHeader(sb, true);
/// <summary>
/// Outputs an "auto-generated" header to a string builder.
/// </summary>
/// <param name="sb">The string builder.</param>
/// <param name="includeVersion">Flag indicating whether the tool version number should be included in the output.</param>
public static void WriteHeader(StringBuilder sb, bool includeVersion)
{
sb.Append("//------------------------------------------------------------------------------\n");
sb.Append("// <auto-generated>\n");
sb.Append("// This code was generated by a tool.\n");
sb.Append("//\n");
sb.AppendFormat("// Umbraco.ModelsBuilder.Embedded v{0}\n", ApiVersion.Current.Version);
if (includeVersion)
{
sb.AppendFormat("// Umbraco.ModelsBuilder.Embedded v{0}\n", ApiVersion.Current.Version);
}
else
{
sb.Append("// Umbraco.ModelsBuilder.Embedded\n");
}
sb.Append("//\n");
sb.Append("// Changes to this file will be lost if the code is regenerated.\n");
sb.Append("// </auto-generated>\n");
@@ -29,7 +29,7 @@ internal class AuditRepository : EntityRepositoryBase<int, IAuditItem>, IAuditRe
List<LogDto>? dtos = Database.Fetch<LogDto>(sql);
return dtos.Select(x => new AuditItem(x.NodeId, Enum<AuditType>.Parse(x.Header), x.UserId ?? Constants.Security.UnknownUserId, x.EntityType, x.Comment, x.Parameters)).ToList();
return dtos.Select(x => new AuditItem(x.NodeId, Enum<AuditType>.Parse(x.Header), x.UserId ?? Constants.Security.UnknownUserId, x.EntityType, x.Datestamp, x.Comment, x.Parameters)).ToList();
}
public void CleanLogs(int maximumAgeOfLogsInMinutes)
@@ -104,7 +104,7 @@ internal class AuditRepository : EntityRepositoryBase<int, IAuditItem>, IAuditRe
totalRecords = page.TotalItems;
var items = page.Items.Select(
dto => new AuditItem(dto.NodeId, Enum<AuditType>.ParseOrNull(dto.Header) ?? AuditType.Custom, dto.UserId ?? Constants.Security.UnknownUserId, dto.EntityType, dto.Comment, dto.Parameters)).ToList();
dto => new AuditItem(dto.NodeId, Enum<AuditType>.ParseOrNull(dto.Header) ?? AuditType.Custom, dto.UserId ?? Constants.Security.UnknownUserId, dto.EntityType, dto.Datestamp, dto.Comment, dto.Parameters)).ToList();
// map the DateStamp
for (var i = 0; i < items.Count; i++)
@@ -144,12 +144,12 @@ internal class AuditRepository : EntityRepositoryBase<int, IAuditItem>, IAuditRe
protected override IAuditItem? PerformGet(int id)
{
Sql<ISqlContext> sql = GetBaseQuery(false);
sql.Where(GetBaseWhereClause(), new { Id = id });
sql.Where(GetBaseWhereClause(), new { id = id });
LogDto? dto = Database.First<LogDto>(sql);
return dto == null
? null
: new AuditItem(dto.NodeId, Enum<AuditType>.Parse(dto.Header), dto.UserId ?? Constants.Security.UnknownUserId, dto.EntityType, dto.Comment, dto.Parameters);
: new AuditItem(dto.NodeId, Enum<AuditType>.Parse(dto.Header), dto.UserId ?? Constants.Security.UnknownUserId, dto.EntityType, dto.Datestamp, dto.Comment, dto.Parameters);
}
protected override IEnumerable<IAuditItem> PerformGetAll(params int[]? ids) => throw new NotImplementedException();
@@ -162,7 +162,7 @@ internal class AuditRepository : EntityRepositoryBase<int, IAuditItem>, IAuditRe
List<LogDto>? dtos = Database.Fetch<LogDto>(sql);
return dtos.Select(x => new AuditItem(x.NodeId, Enum<AuditType>.Parse(x.Header), x.UserId ?? Constants.Security.UnknownUserId, x.EntityType, x.Comment, x.Parameters)).ToList();
return dtos.Select(x => new AuditItem(x.NodeId, Enum<AuditType>.Parse(x.Header), x.UserId ?? Constants.Security.UnknownUserId, x.EntityType, x.Datestamp, x.Comment, x.Parameters)).ToList();
}
protected override Sql<ISqlContext> GetBaseQuery(bool isCount)
@@ -184,7 +184,7 @@ internal class AuditRepository : EntityRepositoryBase<int, IAuditItem>, IAuditRe
return sql;
}
protected override string GetBaseWhereClause() => "id = @id";
protected override string GetBaseWhereClause() => "umbracoLog.id = @id";
protected override IEnumerable<string> GetDeleteClauses() => throw new NotImplementedException();
}

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