Compare commits

...
120 Commits
Author SHA1 Message Date
Andy Butland 0a98a47db0 Update Examine to 3.8.0. 2026-06-07 21:17:09 +02:00
Andy Butland 18a9da080a Bumped version to 13.15.0-rc 2026-04-30 08:07:45 +02:00
Andy Butland 6104efdd47 Merge branch 'release/13.14.0' into v13/dev 2026-04-30 08:06:57 +02:00
Andy Butland 7d400e7742 Bumped version to 13.14.0 2026-04-30 06:36:08 +02:00
Andy Butland 8295f833f8 Bump version to 13.14.0-rc3. 2026-04-23 16:40:12 +02:00
8d6645b0ef Surface controllers: validate redirect url in public surface controllers (v13) (#22565)
* Fix: prevent open redirect in public surface controllers by validating RedirectUrl with Url.IsLocalUrl

* Fixed spacing in comment.

---------

Co-authored-by: Lan Nguyen Thuy <lnt@umbraco.dk>
Co-authored-by: Andy Butland <abutland73@gmail.com>
2026-04-23 16:39:31 +02:00
a0cb9b2826 Surface controllers: validate redirect url in public surface controllers (v13) (#22565)
* Fix: prevent open redirect in public surface controllers by validating RedirectUrl with Url.IsLocalUrl

* Fixed spacing in comment.

---------

Co-authored-by: Lan Nguyen Thuy <lnt@umbraco.dk>
Co-authored-by: Andy Butland <abutland73@gmail.com>
2026-04-23 16:37:53 +02:00
Andy Butland b2ff910a92 Bumped version to 13.14.0-rc2. 2026-04-17 14:03:37 +02:00
GinoandAndy Butland b70d2b482d Dependencies: Pin System.Security.Cryptography.Xml to resolve vulnerability warning (Umbraco 13) (#22513)
* Update System.Security.Cryptography.Xml - fix vulnerability

* Update Directory.Packages.props

use correct/latest version of the system.security.cryptohraphy.xml package.

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

---------

Co-authored-by: Andy Butland <abutland73@gmail.com>
2026-04-17 14:02:46 +02:00
ac22eef92f Dependencies: Pin System.Security.Cryptography.Xml to resolve vulnerability warning (Umbraco 13) (#22513)
* Update System.Security.Cryptography.Xml - fix vulnerability

* Update Directory.Packages.props

use correct/latest version of the system.security.cryptohraphy.xml package.

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

---------

Co-authored-by: Andy Butland <abutland73@gmail.com>
2026-04-17 11:59:55 +00:00
24ef23e133 Backport #21910 to V13 (#22314)
* Backport #21910 to V13

* Update src/Umbraco.Infrastructure/Search/IndexingNotificationHandler.ContentType.cs

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

* Backport #21856 as well

* Fix issue where removing a property type wasn't detecting and returning the expected change type.

---------

Co-authored-by: Andy Butland <abutland73@gmail.com>
2026-04-07 09:07:07 +02:00
Andy Butland c8fb306e67 Bumped version to 13.14.0-rc. 2026-03-12 06:46:44 +01:00
Andy Butland 667c2f5a47 Set version to 13.13.1. 2026-03-06 11:58:38 +01:00
Andy ButlandandGitHub 8230efd722 Media Picker: Fix media picker folder navigation with "ignore user start nodes" enabled (closes #21840 for 13) (#21899)
Fix media picker folder navigation with "ignore user start nodes" enabled.
2026-03-06 11:02:35 +01:00
Ronald BarendseandGitHub f420b0b894 Remove legacy Windows path length checks and related tests (#21884)
Removed explicit 260-character path length checks from PhysicalFileSystem.GetFullPath and deleted associated unit tests. Updated tests to focus on path normalization and validity, and improved path assertions for clarity and cross-platform compatibility. No longer enforce or test for legacy Windows path length restrictions.
2026-02-24 11:18:11 +01:00
Andy Butland b196ee89b7 Merge branch 'release/13.13' into v13/dev
# Conflicts:
#	version.json
2025-12-18 06:39:51 +01:00
Andy Butland b998afcd93 Merge branch 'release/13.13' of https://github.com/umbraco/Umbraco-CMS into release/13.13 2025-12-17 15:59:36 +01:00
Andy Butland 8c0bdb4b0b Bump version to 13.13.0. 2025-12-17 15:59:23 +01:00
66d090b4e3 Media picker: Fix image selection after upload when media picker presents multiple pages (closes #21115) (#21117)
* Fix image selection after upload when media picker presents multiple pages.

* Apply suggestions from code review

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

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-12-17 08:25:03 +01:00
Andy Butland b779123946 Members: Fix GetLockoutEnd to return DateTimeOffSet max rather than DateTime max (closes #21155) (#21157)
Fix GetLockoutEnd to return DateTimeOffSet max rather than DateTime max, to align with type of function result and avoid chance of overflow when timezone is minus UTC.
2025-12-16 07:04:56 +01:00
Andy ButlandandGitHub ed36b31576 Members: Fix GetLockoutEnd to return DateTimeOffSet max rather than DateTime max (closes #21155) (#21157)
Fix GetLockoutEnd to return DateTimeOffSet max rather than DateTime max, to align with type of function result and avoid chance of overflow when timezone is minus UTC.
2025-12-16 05:44:49 +00:00
Andy Butland 4871c0bab1 Merge branch 'release/13.13' into v13/dev
# Conflicts:
#	version.json
2025-12-11 07:17:23 +01:00
bfe567f075 Updates dependencies to latest patch for 13.13 (#21108)
* Update server-side dependencies to latest patch releases.

* Update client-side dependencies.

* Bumped version to 13.13.0-rc3.

* Reverted change to node version.

* Apply suggestions from code review

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

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-12-10 14:12:44 +00:00
Andy Butland 328c43c225 Merge branch 'release/13.12.1' into release/13.13
# Conflicts:
#	version.json
2025-12-09 06:25:00 +01:00
Andy Butland 56de595457 Merge branch 'release/13.12.1' into v13/dev
# Conflicts:
#	version.json
2025-12-09 06:24:24 +01:00
Andy ButlandandGitHub 7505efd433 Merge commit from fork
* Ensure imported dictionary item is only processed from the expected temporary uploads folder.

* Ensured content type upload input is only a file and not a string that can be interpretted as a file path.

* Amend dictionary import to extract file name and prepend path rather than rely on provided path.

* Tidied usings.

* Ensure file name cannot contain path separator characters.
2025-12-09 05:15:53 +01:00
Andy Butland 934126ae6e Bump version to 13.13.0-rc2. 2025-12-05 15:25:16 +01:00
Bjarne FyrstenborgandAndy Butland 632f04afd2 Rich text Editor: Ensure readonly attribute has been observed (#21004)
* Set toolbar and readonly when readonly attribute has been observed

* Simplify
2025-12-05 15:24:27 +01:00
Bjarne FyrstenborgandGitHub edae01a152 Rich text Editor: Ensure readonly attribute has been observed (#21004)
* Set toolbar and readonly when readonly attribute has been observed

* Simplify
2025-12-05 15:22:12 +01:00
d418f4f4d5 Media Picker: Present root nodes for users with more than one media root node assigned (closes #20967) (#20993)
* Presented root nodes in media picker for users with more than one media root node assigned.

* Apply suggestions from code review

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

* Improved variable names.

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-12-01 13:11:34 +00:00
Andy ButlandandGitHub da677c42ed Media Picker Fixed folder navigation when returning from an empty folder (closes #20975) (#20987)
* Fixed media picker folder navigation when returning from an empty folder.

* Applied suggestions from code review.
2025-12-01 12:59:12 +00:00
Andy ButlandandGitHub 467db7736c Content picker: Remove multi-node content picker entries by UDI to account for index not aligning with stored values (#20950)
* Remove multi-node content picker entries by udi to account for index not aligning with stored values.

* Handled issues raised in code review.
2025-12-01 12:49:19 +00:00
Bjarne FyrstenborgandGitHub aced6916ff Content editing: Correct handling of read-only of name field when A action is removed in SendingContentNotification (#20072)
Set header name to readonly
2025-12-01 06:36:31 +00:00
Bjarne FyrstenborgandGitHub dc0005f394 Block Grid: Streamline readonly with other property editors (#21007)
* Align cursor style and readonly mode for other property editors

* Show disabled actions as in other property editors when readonly

* Disable sortmode as well when readonly

* Formatting

* Ensure delete action remains disabled if readonly
2025-12-01 05:53:28 +00:00
Andy ButlandandGitHub 0da1146fcd Content picker: Allow selections on a content picker on a read-only document to be opened (closes #20816) (#20819)
Allow selections on a content picker on a read-only document to be opened.
2025-11-24 16:50:28 +00:00
a7b4481c6c Redirects: Fix self referencing redirects for 13 (#20908)
* Ported fix to 13

* Mocking context and cache

* Removing unused parameter in constructor

* Removed a couple of unused variables in the tests.

---------

Co-authored-by: Andy Butland <abutland73@gmail.com>
2025-11-20 15:29:58 +00:00
Andy Butland 90ec85386b Merge branch 'release/13.12' into release/13.12.1
# Conflicts:
#	version.json
2025-11-20 06:47:36 +01:00
Andy Butland 9baaad40d1 Merge branch 'release/13.12' into v13/dev
# Conflicts:
#	version.json
2025-11-20 06:46:39 +01:00
Andy Butland 7dcc7303c2 Bumped version to 13.12.0. 2025-11-19 18:49:49 +01:00
Andy Butland 7edc6ec11d Bump version to 13.12.1. 2025-11-17 09:30:40 +01:00
Andy Butland f2589b5af5 Fix memory leak with IOptionsMonitor.OnChange and non-singleton registered components (closes #20709 for 13) (#20722)
* Fix memory leak with IOptionsMonitor.OnChange and non-singleton registered components.

* Added XML docs.

* Apply fix to DeliveryApiContentIndexingNotificationHandler.

* Dispose disposable data editors in ValueEditorCache.

* Removed unnecessary refactoring and clarified code comments.
2025-11-06 17:30:31 +01:00
Andy ButlandandGitHub 23b09b1a7e Fix memory leak with IOptionsMonitor.OnChange and non-singleton registered components (closes #20709 for 13) (#20722)
* Fix memory leak with IOptionsMonitor.OnChange and non-singleton registered components.

* Added XML docs.

* Apply fix to DeliveryApiContentIndexingNotificationHandler.

* Dispose disposable data editors in ValueEditorCache.

* Removed unnecessary refactoring and clarified code comments.
2025-11-06 15:02:57 +00:00
Andy Butland 951f9a9e46 Bump version to 13.12.0-rc2. 2025-11-06 10:41:41 +01:00
Andy Butland 8949fd2d22 Dependencies: Update dotnet sdk and node development dependency to latest secure version of current major (13) (#20729)
Update dotnet sdk and node development dependency to latest secure version of current major.
2025-11-05 21:25:44 +01:00
Andy ButlandandGitHub 5d6f4f2135 Dependencies: Update dotnet sdk and node development dependency to latest secure version of current major (13) (#20729)
Update dotnet sdk and node development dependency to latest secure version of current major.
2025-11-05 21:24:27 +01:00
Sven GeusensandAndy Butland 21520eebd6 Webhooks: Register OutputExpansionStrategy for webhooks if Delivery API is not enabled (closes #20272 for 13) (#20721)
V13 implementation off #20559 (Webhooks: Register OutputExpansionStrategy for webhooks if Delivery API is not enabled)
2025-11-03 19:11:05 +01:00
Sven GeusensandGitHub 87e20ceec6 Webhooks: Register OutputExpansionStrategy for webhooks if Delivery API is not enabled (closes #20272 for 13) (#20721)
V13 implementation off #20559 (Webhooks: Register OutputExpansionStrategy for webhooks if Delivery API is not enabled)
2025-11-03 19:10:14 +01:00
Laura NetoandGitHub f7b874d3ef Dependency track for V13 pipeline (#20702)
* Dependency track for V13 pipeline

* Rename bom-backend to bom-build
2025-10-31 16:03:14 +01:00
ArjanandAndy Butland dedc77c883 Backoffice Caching: Added cache buster for block list stylesheets (closes #20683) (#20686)
Fixed, added cache buster to Block List Editor stylesheets
2025-10-30 16:24:33 +01:00
ArjanandGitHub 8110b80318 Backoffice Caching: Added cache buster for block list stylesheets (closes #20683) (#20686)
Fixed, added cache buster to Block List Editor stylesheets
2025-10-30 16:23:37 +01:00
Andy Butland 9de905cba3 Bumped version to 13.13.0-rc. 2025-10-29 08:02:54 +01:00
Andy ButlandandGitHub 802fa70154 Notifications: Populate IncludeDescendants on ContentPublishedNotification when publishing branch (#20578)
Populate IncludeDescendants on ContentPublishedNotification when publishing branch.
2025-10-22 13:23:19 +02:00
bhcoderandGitHub ebf397702d Grid (Legacy): Fix for RTE drag and drop issue (closes #16591) (#20498)
* #16591- Fix RTE drag and drop issue in Grid Layout (Legacy)

* Change case for variable
2025-10-14 16:07:54 +02:00
388e5f9239 Dependencies: Fixed dependency conflicts when installing Microsoft.EntityFrameworkCore.Design (closes #17839) (#20480)
* Add explicit references to Microsoft.CodeAnalysis.* packages to fix conflicts when installing Microsoft.EntityFrameworkCore.Design

* Disable CPM for UI project to better reflect consumers

* Update src/Umbraco.Infrastructure/Umbraco.Infrastructure.csproj

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

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-10-13 11:19:32 +02:00
Andy Butland d20cdd4930 Merge branch 'v13/dev' of https://github.com/umbraco/Umbraco-CMS into v13/dev 2025-10-09 07:58:28 +02:00
Andy Butland a604b2053e Merge branch 'release/13.11' into v13/dev
# Conflicts:
#	version.json
2025-10-09 07:58:11 +02:00
Andy Butland c177c305ba Bumped version to 13.11.0. 2025-10-09 06:40:37 +02:00
e840a0c7c4 Do not convert calculated lockedOutUntil time to UTC (#17007)
* Do not convert calculated lockedOutUntil time to UTC

* Handle conversion to UTC when we know we have server time values in IdentityMapDefinition for members.

---------

Co-authored-by: Andy Butland <abutland73@gmail.com>
2025-10-08 16:15:36 +02:00
c9d5c7f95b Repositories: v13 UserService.GetAllInGroup fix (#20425)
* Fix: Used FetchByGroups to batch queries to stop SQL limit bug

* Tidied up indentation.

---------

Co-authored-by: Andy Butland <abutland73@gmail.com>
2025-10-08 13:44:32 +00:00
Andy ButlandandGitHub 4c9d02e90b Configuration: Fixed integer overflow in setting MaxRequestBodySize and MultipartBodyLengthLimit (#20348)
Fixed integer overflow in setting MaxRequestBodySize and MultipartBodyLengthLimit.
2025-10-02 13:34:47 +02:00
Andy Butland d39395a1ff Bump version to 13.11.0-rc2. 2025-10-02 09:51:12 +02:00
Andy Butlandandkjac d45ddc315a Caching: Fixes regression of the caching of null representations for missing dictionary items (#20344)
* Fixed regression with handling of null representation in cache.

* Added integration test to verify behaviour.

* Use helper methods.

* Amend tests so they verify that cached null values are also removed

---------

Co-authored-by: kjac <kja@umbraco.dk>
2025-10-02 09:50:07 +02:00
7d87ce35bc Caching: Fixes regression of the caching of null representations for missing dictionary items (#20344)
* Fixed regression with handling of null representation in cache.

* Added integration test to verify behaviour.

* Use helper methods.

* Amend tests so they verify that cached null values are also removed

---------

Co-authored-by: kjac <kja@umbraco.dk>
2025-10-02 07:46:40 +00:00
mcl-szandGitHub d4805faee7 Prevent RTE being marked as dirty when content contains empty paragraph (#20335)
Prevent RTE being marked as dirty when content containt empty paragraph

In Tinymce, an empty paragraph is marked with a non-breaking space character.
When saving the content, the DOMParser replaces this empty non-breaking space character with an &nbsp; entity.
This causes the editor to always be dirty when the content contains an empty paragraph.
By making these the same befor the comparison, this is prevented.
2025-10-01 16:05:57 +02:00
3deccdbcf3 Dependencies: Update ImageSharp to latest patch releases (13) (#20275)
Update ImageSharp dependencies.

Co-authored-by: Kenn Jacobsen <kja@umbraco.dk>
2025-09-29 08:09:01 +00:00
9ad24ac2a7 [v13] Media Picker: implement paging using getPagedChildren (no server changes) (#20202)
* Avoid throwing an exception on getting references when migrating content with changed data types.

* Revert and handle exception at the consumer side

* Clean up

* Fix issue 12364

fix bug for Media Picker is slow when you have a large number of images at the root folder #12364

* Revert "Fix issue 12364"

This reverts commit 4d867345eb.

* Media Picker: use getPagedChildren with paging & sorting

Replaces folder view calls to entityResource.getChildren with
entityResource.getPagedChildren in MediaPickerController.

- Adds orderBy/Direction and increases default pageSize to 200
- Resets pagination when entering folders or clearing search
- Updates changePagination to work for both search and folder views
- Keeps legacy behaviour for searchMedia (no breaking server changes)

* Update mediapicker.controller.js

* Update mediapicker.controller.js

* Update mediapicker.controller.js

* Update mediapicker.controller.js

* Fix default values for paging.

* Refactor Media Picker controller: simplify pagination and clarify comments

- Removed unnecessary helper methods (pickPositive / pickNonNegative) and assign pagination values directly,
  as backend always provides valid positive/ non-negative numbers.
- Renamed or removed review-only comments to keep codebase clean.
- Clarified purpose of resetting `vm.searchOptions.filter` to explain why the filter is cleared after loading items.
- Adjusted indentation and minor formatting for consistency.

* Media Picker: fix lost paging on reopen by chaining navigation promises

Ensures the Media Picker always loads a paged result when restoring the last
visited folder.

- run(), ensureWithinStartNode(), gotoStartNode(), and gotoFolder() now return
  and chain the same promise flow.
- gotoFolder() resolves path → sets current folder → resets pagination →
  calls getChildren() (which uses getPagedChildren).
- Fixed cases where reopening the picker showed all items with no pager.
- Kept existing UX: filter is cleared on folder navigation to start unfiltered.
- Minor indentation/formatting cleanups.

* Tidied up indentation.

---------

Co-authored-by: Andy Butland <abutland73@gmail.com>
Co-authored-by: Kenn Jacobsen <kja@umbraco.dk>
2025-09-26 07:34:15 +00:00
Andy Butland 0751728b05 Merge branch 'release/13.10.1' into release/13.11
# Conflicts:
#	version.json
2025-09-23 07:08:09 +02:00
Andy Butland e504011b76 Merge branch 'release/13.10.1' into v13/dev
# Conflicts:
#	version.json
2025-09-23 07:06:44 +02:00
Andy ButlandandGitHub 4d9ea1e2e3 Property editors: Ensure values no longer allowed as options in checkbox list configuration are removed from the value, such that they will be removed from the persisted data when saved (#20208)
Ensure values no longer allowed as options in checkbox list configuration are removed from the value, such that they will be removed from the persisted data when saved.
2025-09-22 10:48:36 +02:00
6796829908 Querying extensions: Allow ContentAtRoot() to accept culture (#20129)
* Allow ContentAtRoot to accept culture

* Update comments about old TypedContentAtRoot

* Remove old method as new is optional parameter

* Change binary breaking change

* Obsolete methods

* Reverted to non-optional overloads.

* Used nullable string.

---------

Co-authored-by: Andy Butland <abutland73@gmail.com>
2025-09-22 08:27:41 +00:00
67f7d99696 Allow fetching multiple content and media by Guid and Udi (#15289)
* Allow fetching content by Guid and Udi

* Get media by Guid and Udi ids

* Fix array parameters

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

---------

Co-authored-by: Ronald Barendse <ronald@barend.se>
2025-09-20 18:56:55 +02:00
Rick ButterfieldandGitHub 27e3184550 Add support for UrlMode parameter in HtmlLocalLinkParser (#20200)
* Add support for `UrlMode` in `HtmlLocalLinkParser`

* Fix broken tests
2025-09-20 12:48:50 +02:00
Rick ButterfieldandGitHub c98fd7b219 UX: Fix overflow of umb-editor-header if the description is locked and too long (#20190)
Fix overflow of `umb-editor-header` if the description is locked and too long
2025-09-19 08:46:17 +02:00
Kenn JacobsenandGitHub 301be352ae Make notification service able to handle segmented content (#20160) 2025-09-17 06:51:10 +02:00
Andy Butland 7c180f6e4f Merge branch 'v13/dev' of https://github.com/umbraco/Umbraco-CMS into v13/dev
# Conflicts:
#	src/Umbraco.Core/Models/DeliveryApi/IApiContentRoute.cs
2025-09-16 12:22:58 +02:00
e119186e34 Avoid throwing an exception on getting references when migrating content with changed data types (#20079)
* Avoid throwing an exception on getting references when migrating content with changed data types.

* Revert and handle exception at the consumer side

* Clean up

---------

Co-authored-by: Kenn Jacobsen <kja@umbraco.dk>
2025-09-16 09:58:07 +00:00
Kenn JacobsenandGitHub a282002c30 Support querystring and anchor for local links in Delivery API output (#20142)
* Support querystring and anchor for local links in Delivery API output

* Add default implementation for backwards compat

* Add default implementation for backwards compat (also on the interface)

* Fix default implementation
2025-09-16 08:25:22 +02:00
Andy Butland a2c36d602f Bump version to 13.10.1. 2025-09-16 07:55:33 +02:00
Andy Butland a45917f132 Update Smidge dependencies. 2025-09-16 07:54:56 +02:00
Andy Butland fe358b8eab Merge branch 'v13/bugfix/18475-locallinks-parsing' of https://github.com/umbraco/Umbraco-CMS into v13/dev 2025-09-16 06:47:56 +02:00
kjac 60f84f7378 Support querystring and anchor for local links in Delivery API output 2025-09-15 19:54:00 +02:00
Andy ButlandandGitHub 45f7b7ad98 Retain original backoffice location on login after timeout (#19984)
Retain original backoffice location on login after timeout.
2025-09-15 17:53:17 +00:00
85aa6d3b57 Add Arabic (ar) backoffice translation (#19896)
* feat: Add Arabic (ar) backoffice translation

* Make Arabic language general until having special words for other
countries.

* Corrected the language header

---------

Co-authored-by: Andy Butland <abutland73@gmail.com>
2025-08-15 09:06:51 +00:00
Andy Butland 33e201c4b5 Merge branch 'release/13.10' into v13/dev
# Conflicts:
#	version.json
2025-08-14 09:48:50 +01:00
Andy Butland c2890e15c3 Bumped version to 13.10.0 2025-08-14 07:39:22 +01:00
784e09e240 Use a regex to filter our invalid culture codes rather than relying on the culture being installed on the operating system (#19821)
* Use a regex to filter our invalid culture codes rather than relying on the culture being installed on the operating system.

* Update to more restrictive regex

Co-authored-by: Nuklon <Nuklon@users.noreply.github.com>

---------

Co-authored-by: Nuklon <Nuklon@users.noreply.github.com>
2025-08-06 10:34:08 +02:00
214f3fbc64 Umbraco Engage UmbracoUrlAlias Fix - Fixes #19654 (#19827)
* Fixes #19654

Adds the propertyAlias to the VariationContext so that products implementing the GetSegment method are aware which propertyAlias it's being called for

* Re-implement original variation context for backwards compatibility

* Fixes hidden overload method

Ensures the `GetSegment` method overload is not hidden when a null `propertyAlias` is passed.

* Resolve backward compatibility issues.

* Improved comments.

---------

Co-authored-by: Andy Butland <abutland73@gmail.com>
2025-08-04 11:40:00 +02:00
b622e7e4fb Content picker search with start node configured not taking user start nodes into account (#19800)
* Fix users being able to see nodes they don't have access to when using the picker search

* Readability and naming improvements

* Additional fixes

* Adjust tests

* Additional fixes

* Small improvement

* Replaced the root ids with constants

* Update src/Umbraco.Web.BackOffice/Trees/MemberTreeController.cs

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

---------

Co-authored-by: Andy Butland <abutland73@gmail.com>
2025-08-01 11:50:14 +02:00
Andy Butland a352c42742 Merge branch 'release/13.9.3' into release/13.10
# Conflicts:
#	version.json
2025-07-29 07:17:06 +02:00
Andy Butland 8eff28b80c Merge branch 'release/13.9.3' into v13/dev
# Conflicts:
#	version.json
2025-07-29 07:16:23 +02:00
0d94f8bfaf Fix issue with use of EF Core scopes within notification handlers (take 2 - handling scopes with a base parent) (#19797)
* Add integration tests that shows the problem

* Fix the problem and add explenation

* Improved comments slightly to help when we come back here!
Moved tests alongside existing ones related to scopes.
Removed long running attribute from tests (they are quite fast).

* Fixed casing in comment.

---------

Co-authored-by: Andy Butland <abutland73@gmail.com>
2025-07-25 13:09:27 +02:00
AaronandGitHub 509a2e18d0 The filter param should be urlencoded (#19774) 2025-07-23 11:54:22 +02:00
Andy Butland 22f748161a Bumped version to 13.11.0-rc. 2025-07-22 10:11:54 +02:00
Andy ButlandandGitHub 59ad07209b Retrieve only user external logins when invalidate following removal of backoffice external user login (#19766)
* Retrieve only user external logins when invalidate following removal of backoffice external user login.

* Improved variable name.
2025-07-22 07:50:05 +00:00
Kenn JacobsenandGitHub 67abecc252 Add defensive coding to the member application initializer (#19760) 2025-07-21 12:24:51 +02:00
Andy ButlandandGitHub 417f15197e Parse update date before sorting in media list view (#19711)
* Parse update date before sorting in media list view.

* Moved function placement.
2025-07-16 09:45:17 +01:00
ce40103c4f Add support for programmatic creation of property types providing the data type key (#19720)
* Add support for programmatic creation of property types providing the data type key.

* Add integration tests

---------

Co-authored-by: kjac <kja@umbraco.dk>
2025-07-15 13:57:48 +02:00
Andy ButlandandGitHub 2748fdfc48 Adds variation by the header name Accept-Language to the delivery API output cache policy (#19709)
* Adds variation by the header name Accept-Language to the develivery API output cache policy

* Removed obsolete constructor (not necessary as the class is internal).

* Introduce contants for header names.
2025-07-11 15:51:05 +02:00
Andy ButlandandGitHub 53cc663bde Register no-op implementation of IMemberPartialViewCacheInvalidator in headless setups (#19666)
* Register no-op implementation of IMemberPartialViewCacheInvalidator in headless setups.

* Tidied usings.
2025-07-08 15:40:53 +02:00
Andy ButlandandGitHub 13a2cd71c4 Clear member cache by older user name when member user name is updated. (#19672)
* Clear member cache by older user name when member user name is updated.

* Added unit test.
2025-07-07 14:01:36 +02:00
kowsandGitHub a60ccd389b #16772 partial fix backoffice redirect after login (#19663)
* #16772 partial fix backoffice redirect after login

* #16772 partial fix backoffice OpenId redirect after login
2025-07-07 10:51:50 +02:00
Andy Butland fd95dc3915 Merge branch 'release/13.9.2' into v13/dev
# Conflicts:
#	version.json
2025-07-02 06:29:04 +02:00
Andy ButlandandGitHub 990e379ea8 Ensures that null values aren't used to create a CompositeStringStringKey (#19646)
Ensures that null values aren't used to create a CompositeStringStringKey.
2025-07-01 12:11:04 +00:00
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 Butland a01382d756 Merge branch 'release/13.9' into v13/dev 2025-06-05 10:00:12 +02:00
Andy Butland 97cc3ca581 Bumped version to 13.9.0. 2025-06-05 06:24:08 +02: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 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
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
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
150 changed files with 7369 additions and 805 deletions
+21 -20
View File
@@ -12,23 +12,24 @@
</ItemGroup>
<!-- Microsoft packages -->
<ItemGroup>
<PackageVersion Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="8.0.11" />
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation" Version="8.0.11" />
<PackageVersion Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="8.0.22" />
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation" Version="8.0.22" />
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="4.10.0" />
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="4.10.0" />
<PackageVersion Include="Microsoft.CodeAnalysis.Workspaces.MSBuild" Version="4.10.0" />
<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.EntityFrameworkCore.Sqlite" Version="8.0.22" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.SqlServer" Version="8.0.22" />
<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.1" />
<PackageVersion Include="Microsoft.Extensions.FileProviders.Embedded" Version="8.0.11" />
<PackageVersion Include="Microsoft.Extensions.FileProviders.Embedded" Version="8.0.22" />
<PackageVersion Include="Microsoft.Extensions.FileProviders.Physical" Version="8.0.0" />
<PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="8.0.0" />
<PackageVersion Include="Microsoft.Extensions.Http" Version="8.0.1" />
<PackageVersion Include="Microsoft.Extensions.Identity.Core" Version="8.0.11" />
<PackageVersion Include="Microsoft.Extensions.Identity.Stores" Version="8.0.11" />
<PackageVersion Include="Microsoft.Extensions.Identity.Core" Version="8.0.22" />
<PackageVersion Include="Microsoft.Extensions.Identity.Stores" Version="8.0.22" />
<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" />
@@ -45,9 +46,9 @@
<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.7.1" />
<PackageVersion Include="Examine.Core" Version="3.7.1" />
<PackageVersion Include="HtmlAgilityPack" Version="1.11.71" />
<PackageVersion Include="Examine" Version="3.8.0" />
<PackageVersion Include="Examine.Core" Version="3.8.0" />
<PackageVersion Include="HtmlAgilityPack" Version="1.11.74" />
<PackageVersion Include="K4os.Compression.LZ4" Version="1.3.8" />
<PackageVersion Include="MailKit" Version="4.8.0" />
<PackageVersion Include="Markdown" Version="2.2.1" />
@@ -55,7 +56,7 @@
<PackageVersion Include="MiniProfiler.AspNetCore.Mvc" Version="4.3.8" />
<PackageVersion Include="MiniProfiler.Shared" Version="4.3.8" />
<PackageVersion Include="ncrontab" Version="3.3.3" />
<PackageVersion Include="Newtonsoft.Json" Version="13.0.3" />
<PackageVersion Include="Newtonsoft.Json" Version="13.0.4" />
<PackageVersion Include="NPoco" Version="5.7.1" />
<PackageVersion Include="NPoco.SqlServer" Version="5.7.1" />
<PackageVersion Include="OpenIddict.Abstractions" Version="4.10.1" />
@@ -73,20 +74,20 @@
<PackageVersion Include="Serilog.Sinks.Async" Version="1.5.0" />
<PackageVersion Include="Serilog.Sinks.File" Version="5.0.0" />
<PackageVersion Include="Serilog.Sinks.Map" Version="1.0.2" />
<PackageVersion Include="SixLabors.ImageSharp" Version="3.1.7" />
<PackageVersion Include="SixLabors.ImageSharp.Web" Version="3.1.3" />
<PackageVersion Include="Smidge.InMemory" Version="4.4.0" />
<PackageVersion Include="Smidge.Nuglify" Version="4.5.1" />
<PackageVersion Include="SixLabors.ImageSharp" Version="3.1.12" />
<PackageVersion Include="SixLabors.ImageSharp.Web" Version="3.1.5" />
<PackageVersion Include="Smidge.InMemory" Version="4.6.0" />
<PackageVersion Include="Smidge.Nuglify" Version="4.6.0" />
<PackageVersion Include="Swashbuckle.AspNetCore" Version="6.9.0" />
</ItemGroup>
<!-- Transitive pinned versions (only required because our direct dependencies have vulnerable versions of transitive dependencies) -->
<ItemGroup>
<!-- Both Microsoft.EntityFrameworkCore.SqlServer and NPoco.SqlServer bring in a vulnerable version of Azure.Identity -->
<PackageVersion Include="Azure.Identity" Version="1.13.1" />
<PackageVersion Include="Azure.Identity" Version="1.13.2" />
<!-- 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.2" />
<PackageVersion Include="System.Security.Cryptography.Xml" Version="8.0.3" />
<!-- 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 -->
@@ -96,8 +97,8 @@
<!-- 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" />
<PackageVersion Include="System.Text.Json" Version="8.0.6" />
<!-- 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" />
<PackageVersion Include="Microsoft.Data.SqlClient" Version="5.2.3" />
</ItemGroup>
</Project>
+81 -26
View File
@@ -25,6 +25,10 @@ parameters:
displayName: Upload API docs
type: boolean
default: false
- name: uploadDependencyTrack
displayName: Upload BOMs to Dependency Track
type: boolean
default: false
- name: forceReleaseTestFilter
displayName: Force to use the release test filters
type: boolean
@@ -93,6 +97,11 @@ stages:
- script: npm ci --no-fund --no-audit --prefer-offline
workingDirectory: src/Umbraco.Web.UI.Client
displayName: Run npm ci (Backoffice)
- powershell: |
npm install --global @cyclonedx/cyclonedx-npm
cyclonedx-npm -o $(Build.ArtifactStagingDirectory)\bom\bom-backoffice.xml --ignore-npm-errors --verbose
displayName: Generate Backoffice UI BOM
workingDirectory: src/Umbraco.Web.UI.Client
- task: gulp@0
displayName: Run gulp build (Backoffice)
inputs:
@@ -106,6 +115,10 @@ stages:
workingDir: src/Umbraco.Web.UI.Login
verbose: false
customCommand: ci
- powershell: |
cyclonedx-npm -o $(Build.ArtifactStagingDirectory)\bom\bom-login.xml --ignore-npm-errors --verbose
displayName: Generate Login UI BOM
workingDirectory: src/Umbraco.Web.UI.Login
- task: npm@1
displayName: Run npm build (Login)
inputs:
@@ -129,6 +142,10 @@ stages:
command: build
projects: $(solution)
arguments: '--configuration $(buildConfiguration) --no-restore --property:ContinuousIntegrationBuild=true --property:GeneratePackageOnBuild=true --property:PackageOutputPath=$(Build.ArtifactStagingDirectory)/nupkg'
- powershell: |
dotnet tool install --global CycloneDX
dotnet-CycloneDX $(solution) --output $(Build.ArtifactStagingDirectory)/bom --filename bom-dotnet.xml
displayName: 'Generate Backend BOM'
- task: PublishPipelineArtifact@1
displayName: Publish nupkg
inputs:
@@ -139,6 +156,35 @@ stages:
inputs:
targetPath: $(Build.SourcesDirectory)
artifactName: build_output
- task: PublishPipelineArtifact@1
displayName: Publish Backend BOM
inputs:
targetPath: $(Build.ArtifactStagingDirectory)/bom
artifactName: bom-build
- stage: E2E_BOM
displayName: E2E Tests BOM Generation
dependsOn: []
jobs:
- job:
displayName: E2E Generate BOM
pool:
vmImage: "ubuntu-latest"
steps:
- checkout: self
fetchDepth: 500
- template: templates/e2e-install.yml
parameters:
nodeVersion: ${{ variables.nodeVersion }}
npm_config_cache: ${{ variables.npm_config_cache }}
- powershell: |
npm install --global @cyclonedx/cyclonedx-npm
cyclonedx-npm -o $(Build.ArtifactStagingDirectory)/bom/bom-e2e.xml --ignore-npm-errors --verbose
displayName: Generate E2E Tests BOM
workingDirectory: tests/Umbraco.Tests.AcceptanceTest
- publish: $(Build.ArtifactStagingDirectory)/bom
artifact: bom-e2e
displayName: 'Publish E2E BOM'
- stage: Build_Docs
condition: and(succeeded(), or(eq(dependencies.Build.outputs['A.build.NBGV_PublicRelease'], 'True'), ${{parameters.buildApiDocs}}))
@@ -464,37 +510,18 @@ stages:
artifact: nupkg
path: $(Agent.BuildDirectory)/app/nupkg
- task: NodeTool@0
displayName: Use Node.js $(nodeVersion)
retryCountOnTaskFailure: 3
inputs:
versionSpec: $(nodeVersion)
- task: UseDotNet@2
displayName: Use .NET SDK from global.json
inputs:
useGlobalJson: true
- pwsh: |
"UMBRACO_USER_LOGIN=$(UMBRACO__CMS__UNATTENDED__UNATTENDEDUSEREMAIL)
UMBRACO_USER_PASSWORD=$(UMBRACO__CMS__UNATTENDED__UNATTENDEDUSERPASSWORD)
URL=$(ASPNETCORE_URLS)" | Out-File .env
displayName: Generate .env
workingDirectory: $(Build.SourcesDirectory)/tests/Umbraco.Tests.AcceptanceTest
# Cache and restore NPM packages
- task: Cache@2
displayName: Cache NPM packages
inputs:
key: 'npm_e2e | "$(Agent.OS)" | $(Build.SourcesDirectory)/tests/Umbraco.Tests.AcceptanceTest/package-lock.json'
restoreKeys: |
npm_e2e | "$(Agent.OS)"
npm_e2e
path: $(npm_config_cache)
- script: npm ci --no-fund --no-audit --prefer-offline
workingDirectory: $(Build.SourcesDirectory)/tests/Umbraco.Tests.AcceptanceTest
displayName: Restore NPM packages
- template: templates/e2e-install.yml
parameters:
nodeVersion: $(nodeVersion)
npm_config_cache: $(npm_config_cache)
PlaywrightUserEmail: $(UMBRACO__CMS__UNATTENDED__UNATTENDEDUSEREMAIL)
PlaywrightPassword: $(UMBRACO__CMS__UNATTENDED__UNATTENDEDUSERPASSWORD)
ASPNETCORE_URLS: $(ASPNETCORE_URLS)
# Build application
- pwsh: |
@@ -755,6 +782,34 @@ stages:
searchFolder: "tests/Umbraco.Tests.AcceptanceTest/results"
testRunTitle: "$(Agent.JobName)"
- stage: Dependency_Track
displayName: Dependency Track
dependsOn:
- Build
- E2E_BOM
condition: and(succeeded(), or(eq(dependencies.Build.outputs['A.build.NBGV_PublicRelease'], 'True'), ${{parameters.uploadDependencyTrack}}))
variables:
# Determine Umbraco version based on whether it's a public release or not. If public release, use major version, else use full NuGet package version.
umbracoVersion: $[ iif(eq(stageDependencies.Build.A.outputs['build.NBGV_PublicRelease'], 'True'), stageDependencies.Build.A.outputs['build.NBGV_VersionMajor'], stageDependencies.Build.A.outputs['build.NBGV_NuGetPackageVersion']) ]
jobs:
- template: templates/dependency-track.yml
parameters:
projectName: "Umbraco-CMS"
umbracoVersion: $(umbracoVersion)
projects:
- name: "Backend"
artifact: "bom-build"
bomFilePath: "bom-dotnet.xml"
- name: "Login"
artifact: "bom-build"
bomFilePath: "bom-login.xml"
- name: "Backoffice"
artifact: "bom-build"
bomFilePath: "bom-backoffice.xml"
- name: "E2E"
artifact: "bom-e2e"
bomFilePath: "bom-e2e.xml"
###############################################
## Release
###############################################
+56
View File
@@ -0,0 +1,56 @@
parameters:
- name: projectName
type: string
- name: umbracoVersion
type: string
- name: projects
type: object
jobs:
- job: Create_DT_Project
displayName: Create Dependency Track Project
steps:
- checkout: none
- bash: |
project_id=$(curl --no-progress-meter -H "X-Api-Key: $(DT_API_KEY)" "$(DT_API_URL)/v1/project/lookup?name=${{ parameters.projectName }}&version=${{ parameters.umbracoVersion }}" | jq -r '.uuid')
if [ "$project_id" != "null" ] && [ -n "$project_id" ]; then
echo "Project '${{ parameters.projectName }}' with version '${{ parameters.umbracoVersion }}' already exists (ID: $project_id)."
else
project_id=$(curl --no-progress-meter \
-X PUT "$(DT_API_URL)/v1/project" \
-H "X-Api-Key: $(DT_API_KEY)" \
-H "Content-Type: application/json" \
-d '{"name": "${{ parameters.projectName }}", "version": "${{ parameters.umbracoVersion }}", "collectionLogic": "AGGREGATE_DIRECT_CHILDREN"}' \
| jq -r '.uuid')
if [ -z "$project_id" ] || [ "$project_id" == "null" ]; then
echo "Failed to create project '${{ parameters.projectName }}' version '${{ parameters.umbracoVersion }}'."
exit 1
fi
echo "Created project '${{ parameters.projectName }}' with version '${{ parameters.umbracoVersion }}' (ID: $project_id)."
fi
displayName: Ensure main project exists in Dependency Track
- ${{ each project in parameters.projects }}:
- job:
displayName: Upload ${{ project.name }} BOM
dependsOn: Create_DT_Project
steps:
- checkout: none
- download: current
artifact: ${{ project.artifact }}
displayName: Download ${{ project.artifact }} artifact
- script: |
curl --no-progress-meter --fail-with-body \
-X POST "$(DT_API_URL)/v1/bom" \
-H "X-Api-Key: $(DT_API_KEY)" \
-H "Content-Type: multipart/form-data" \
-F "autoCreate=true" \
-F "projectName=${{ parameters.projectName }}-${{ project.name }}" \
-F "projectVersion=${{ parameters.umbracoVersion }}" \
-F "parentName=${{ parameters.projectName }}" \
-F "parentVersion=${{ parameters.umbracoVersion }}" \
-F "bom=@$(Pipeline.Workspace)/${{ project.artifact }}/${{ project.bomFilePath }}"
displayName: Upload ${{ project.name }} BOM to Dependency Track
+47
View File
@@ -0,0 +1,47 @@
parameters:
- name: nodeVersion
type: string
default: ''
- name: npm_config_cache
type: string
default: ''
- name: PlaywrightUserEmail
type: string
default: ''
- name: PlaywrightPassword
type: string
default: ''
- name: ASPNETCORE_URLS
type: string
default: ''
steps:
- task: NodeTool@0
displayName: Use Node.js $(nodeVersion)
inputs:
versionSpec: $(nodeVersion)
- pwsh: |
"UMBRACO_USER_LOGIN=${{ parameters.PlaywrightUserEmail }}
UMBRACO_USER_PASSWORD=${{ parameters.PlaywrightPassword }}
URL=${{ parameters.ASPNETCORE_URLS }}" | Out-File .env
displayName: Generate .env
workingDirectory: $(Build.SourcesDirectory)/tests/Umbraco.Tests.AcceptanceTest
# Cache and restore NPM packages
- task: Cache@2
displayName: Cache NPM packages
inputs:
key: 'npm_e2e | "$(Agent.OS)" | $(Build.SourcesDirectory)/tests/Umbraco.Tests.AcceptanceTest/package-lock.json'
restoreKeys: |
npm_e2e | "$(Agent.OS)"
npm_e2e
path: ${{ parameters.npm_config_cache }}
- script: npm ci --no-fund --no-audit --prefer-offline
workingDirectory: $(Build.SourcesDirectory)/tests/Umbraco.Tests.AcceptanceTest
displayName: Restore NPM packages
+1 -1
View File
@@ -1,6 +1,6 @@
{
"sdk": {
"version": "8.0.100",
"version": "8.0.415",
"rollForward": "latestFeature"
}
}
@@ -1,5 +1,6 @@
using Microsoft.AspNetCore.OutputCaching;
using Microsoft.AspNetCore.OutputCaching;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Primitives;
using Umbraco.Cms.Core.DeliveryApi;
namespace Umbraco.Cms.Api.Delivery.Caching;
@@ -7,9 +8,13 @@ namespace Umbraco.Cms.Api.Delivery.Caching;
internal sealed class DeliveryApiOutputCachePolicy : IOutputCachePolicy
{
private readonly TimeSpan _duration;
private readonly StringValues _varyByHeaderNames;
public DeliveryApiOutputCachePolicy(TimeSpan duration)
=> _duration = duration;
public DeliveryApiOutputCachePolicy(TimeSpan duration, StringValues varyByHeaderNames)
{
_duration = duration;
_varyByHeaderNames = varyByHeaderNames;
}
ValueTask IOutputCachePolicy.CacheRequestAsync(OutputCacheContext context, CancellationToken cancellationToken)
{
@@ -25,6 +30,7 @@ internal sealed class DeliveryApiOutputCachePolicy : IOutputCachePolicy
context.EnableOutputCaching = requestPreviewService.IsPreview() is false && apiAccessService.HasPublicAccess();
context.ResponseExpirationTimeSpan = _duration;
context.CacheVaryByRules.HeaderNames = _varyByHeaderNames;
return ValueTask.CompletedTask;
}
@@ -19,7 +19,7 @@ public class ByRouteContentApiController : ContentApiItemControllerBase
private readonly IRequestRedirectService _requestRedirectService;
private readonly IRequestPreviewService _requestPreviewService;
private readonly IRequestMemberAccessService _requestMemberAccessService;
private const string PreviewContentRequestPathPrefix = $"/{Constants.DeliveryApi.Routing.PreviewContentPathPrefix}";
private const string PreviewContentRequestPathPrefix = $"/{Umbraco.Cms.Core.Constants.DeliveryApi.Routing.PreviewContentPathPrefix}";
[Obsolete($"Please use the constructor that does not accept {nameof(IPublicAccessService)}. Will be removed in V14.")]
public ByRouteContentApiController(
@@ -5,6 +5,7 @@ using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Primitives;
using Umbraco.Cms.Api.Common.DependencyInjection;
using Umbraco.Cms.Api.Delivery.Accessors;
using Umbraco.Cms.Api.Delivery.Caching;
@@ -33,26 +34,33 @@ public static class UmbracoBuilderExtensions
builder.Services.AddScoped<IRequestStartItemProvider, RequestStartItemProvider>();
builder.Services.AddScoped<RequestContextOutputExpansionStrategy>();
builder.Services.AddScoped<RequestContextOutputExpansionStrategyV2>();
builder.Services.AddScoped<IOutputExpansionStrategy>(provider =>
{
HttpContext? httpContext = provider.GetRequiredService<IHttpContextAccessor>().HttpContext;
ApiVersion? apiVersion = httpContext?.GetRequestedApiVersion();
if (apiVersion is null)
{
return provider.GetRequiredService<RequestContextOutputExpansionStrategyV2>();
}
// V1 of the Delivery API uses a different expansion strategy than V2+
return apiVersion.MajorVersion == 1
? provider.GetRequiredService<RequestContextOutputExpansionStrategy>()
: provider.GetRequiredService<RequestContextOutputExpansionStrategyV2>();
});
builder.Services.AddUnique<IOutputExpansionStrategy>(
provider =>
{
HttpContext? httpContext = provider.GetRequiredService<IHttpContextAccessor>().HttpContext;
ApiVersion? apiVersion = httpContext?.GetRequestedApiVersion();
if (apiVersion is null)
{
return provider.GetRequiredService<RequestContextOutputExpansionStrategyV2>();
}
// V1 of the Delivery API uses a different expansion strategy than V2+
return apiVersion.MajorVersion == 1
? provider.GetRequiredService<RequestContextOutputExpansionStrategy>()
: provider.GetRequiredService<RequestContextOutputExpansionStrategyV2>();
},
ServiceLifetime.Scoped);
builder.Services.AddSingleton<IRequestCultureService, RequestCultureService>();
builder.Services.AddSingleton<IRequestRoutingService, RequestRoutingService>();
builder.Services.AddSingleton<IRequestRedirectService, RequestRedirectService>();
builder.Services.AddSingleton<IRequestPreviewService, RequestPreviewService>();
builder.Services.AddSingleton<IOutputExpansionStrategyAccessor, RequestContextOutputExpansionStrategyAccessor>();
// Webooks register a more basic implementation, remove it.
builder.Services.AddUnique<IOutputExpansionStrategyAccessor, RequestContextOutputExpansionStrategyAccessor>(ServiceLifetime.Singleton);
builder.Services.AddSingleton<IRequestStartItemProviderAccessor, RequestContextRequestStartItemProviderAccessor>();
builder.Services.AddSingleton<IApiAccessService, ApiAccessService>();
builder.Services.AddSingleton<IApiContentQueryService, ApiContentQueryService>();
builder.Services.AddSingleton<IApiContentQueryProvider, ApiContentQueryProvider>();
@@ -108,12 +116,20 @@ public static class UmbracoBuilderExtensions
if (outputCacheSettings.ContentDuration.TotalSeconds > 0)
{
options.AddPolicy(Constants.DeliveryApi.OutputCache.ContentCachePolicy, new DeliveryApiOutputCachePolicy(outputCacheSettings.ContentDuration));
options.AddPolicy(
Constants.DeliveryApi.OutputCache.ContentCachePolicy,
new DeliveryApiOutputCachePolicy(
outputCacheSettings.ContentDuration,
new StringValues([Constants.DeliveryApi.HeaderNames.AcceptLanguage, Constants.DeliveryApi.HeaderNames.StartItem])));
}
if (outputCacheSettings.MediaDuration.TotalSeconds > 0)
{
options.AddPolicy(Constants.DeliveryApi.OutputCache.MediaCachePolicy, new DeliveryApiOutputCachePolicy(outputCacheSettings.MediaDuration));
options.AddPolicy(
Constants.DeliveryApi.OutputCache.MediaCachePolicy,
new DeliveryApiOutputCachePolicy(
outputCacheSettings.MediaDuration,
Constants.DeliveryApi.HeaderNames.StartItem));
}
});
@@ -1,4 +1,4 @@
using Microsoft.OpenApi.Any;
using Microsoft.OpenApi.Any;
using Microsoft.OpenApi.Models;
using Swashbuckle.AspNetCore.SwaggerGen;
using Umbraco.Cms.Api.Delivery.Configuration;
@@ -21,7 +21,7 @@ internal sealed class SwaggerContentDocumentationFilter : SwaggerDocumentationFi
operation.Parameters.Add(new OpenApiParameter
{
Name = "Accept-Language",
Name = Core.Constants.DeliveryApi.HeaderNames.AcceptLanguage,
In = ParameterLocation.Header,
Required = false,
Description = "Defines the language to return. Use this when querying language variant content items.",
@@ -37,7 +37,7 @@ internal sealed class SwaggerContentDocumentationFilter : SwaggerDocumentationFi
operation.Parameters.Add(new OpenApiParameter
{
Name = "Preview",
Name = Core.Constants.DeliveryApi.HeaderNames.Preview,
In = ParameterLocation.Header,
Required = false,
Description = "Whether to request draft content.",
@@ -46,7 +46,7 @@ internal sealed class SwaggerContentDocumentationFilter : SwaggerDocumentationFi
operation.Parameters.Add(new OpenApiParameter
{
Name = "Start-Item",
Name = Core.Constants.DeliveryApi.HeaderNames.StartItem,
In = ParameterLocation.Header,
Required = false,
Description = "URL segment or GUID of a root content item.",
@@ -1,4 +1,4 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc;
using Microsoft.OpenApi.Any;
using Microsoft.OpenApi.Models;
using Swashbuckle.AspNetCore.SwaggerGen;
@@ -63,7 +63,7 @@ internal abstract class SwaggerDocumentationFilterBase<TBaseController>
protected void AddApiKey(OpenApiOperation operation) =>
operation.Parameters.Add(new OpenApiParameter
{
Name = "Api-Key",
Name = Core.Constants.DeliveryApi.HeaderNames.ApiKey,
In = ParameterLocation.Header,
Required = false,
Description = "API key specified through configuration to authorize access to the API.",
@@ -6,6 +6,7 @@ using Umbraco.Cms.Core.Configuration.Models;
using Umbraco.Cms.Core.Events;
using Umbraco.Cms.Core.Notifications;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Sync;
using Umbraco.Cms.Infrastructure.Security;
namespace Umbraco.Cms.Api.Delivery.Handlers;
@@ -16,16 +17,21 @@ internal sealed class InitializeMemberApplicationNotificationHandler : INotifica
private readonly ILogger<InitializeMemberApplicationNotificationHandler> _logger;
private readonly DeliveryApiSettings _deliveryApiSettings;
private readonly IServiceScopeFactory _serviceScopeFactory;
private readonly IServerRoleAccessor _serverRoleAccessor;
private static readonly SemaphoreSlim _locker = new(1);
private static bool _isInitialized = false;
public InitializeMemberApplicationNotificationHandler(
IRuntimeState runtimeState,
IOptions<DeliveryApiSettings> deliveryApiSettings,
ILogger<InitializeMemberApplicationNotificationHandler> logger,
IServiceScopeFactory serviceScopeFactory)
IServiceScopeFactory serviceScopeFactory,
IServerRoleAccessor serverRoleAccessor)
{
_runtimeState = runtimeState;
_logger = logger;
_serviceScopeFactory = serviceScopeFactory;
_serverRoleAccessor = serverRoleAccessor;
_deliveryApiSettings = deliveryApiSettings.Value;
}
@@ -36,34 +42,55 @@ internal sealed class InitializeMemberApplicationNotificationHandler : INotifica
return;
}
// we cannot inject the IMemberApplicationManager because it ultimately takes a dependency on the DbContext ... and during
// install that is not allowed (no connection string means no DbContext)
using IServiceScope scope = _serviceScopeFactory.CreateScope();
IMemberApplicationManager memberApplicationManager = scope.ServiceProvider.GetRequiredService<IMemberApplicationManager>();
if (_deliveryApiSettings.MemberAuthorization?.AuthorizationCodeFlow?.Enabled is not true)
if (_serverRoleAccessor.CurrentServerRole is ServerRole.Subscriber)
{
await memberApplicationManager.DeleteMemberApplicationAsync(cancellationToken);
// subscriber instances should not alter the member application
return;
}
if (ValidateRedirectUrls(_deliveryApiSettings.MemberAuthorization.AuthorizationCodeFlow.LoginRedirectUrls) is false)
try
{
await memberApplicationManager.DeleteMemberApplicationAsync(cancellationToken);
return;
}
await _locker.WaitAsync(cancellationToken);
if (_isInitialized)
{
return;
}
if (_deliveryApiSettings.MemberAuthorization.AuthorizationCodeFlow.LogoutRedirectUrls.Any()
&& ValidateRedirectUrls(_deliveryApiSettings.MemberAuthorization.AuthorizationCodeFlow.LogoutRedirectUrls) is false)
_isInitialized = true;
// we cannot inject the IMemberApplicationManager because it ultimately takes a dependency on the DbContext ... and during
// install that is not allowed (no connection string means no DbContext)
using IServiceScope scope = _serviceScopeFactory.CreateScope();
IMemberApplicationManager memberApplicationManager = scope.ServiceProvider.GetRequiredService<IMemberApplicationManager>();
if (_deliveryApiSettings.MemberAuthorization?.AuthorizationCodeFlow?.Enabled is not true)
{
await memberApplicationManager.DeleteMemberApplicationAsync(cancellationToken);
return;
}
if (ValidateRedirectUrls(_deliveryApiSettings.MemberAuthorization.AuthorizationCodeFlow.LoginRedirectUrls) is false)
{
await memberApplicationManager.DeleteMemberApplicationAsync(cancellationToken);
return;
}
if (_deliveryApiSettings.MemberAuthorization.AuthorizationCodeFlow.LogoutRedirectUrls.Any()
&& ValidateRedirectUrls(_deliveryApiSettings.MemberAuthorization.AuthorizationCodeFlow.LogoutRedirectUrls) is false)
{
await memberApplicationManager.DeleteMemberApplicationAsync(cancellationToken);
return;
}
await memberApplicationManager.EnsureMemberApplicationAsync(
_deliveryApiSettings.MemberAuthorization.AuthorizationCodeFlow.LoginRedirectUrls,
_deliveryApiSettings.MemberAuthorization.AuthorizationCodeFlow.LogoutRedirectUrls,
cancellationToken);
}
finally
{
await memberApplicationManager.DeleteMemberApplicationAsync(cancellationToken);
return;
_locker.Release();
}
await memberApplicationManager.EnsureMemberApplicationAsync(
_deliveryApiSettings.MemberAuthorization.AuthorizationCodeFlow.LoginRedirectUrls,
_deliveryApiSettings.MemberAuthorization.AuthorizationCodeFlow.LogoutRedirectUrls,
cancellationToken);
}
private bool ValidateRedirectUrls(Uri[] redirectUrls)
@@ -1,62 +1,24 @@
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using Umbraco.Cms.Core.DeliveryApi;
using Umbraco.Cms.Core.Models.PublishedContent;
using Umbraco.Extensions;
using Umbraco.Cms.Web.Common.Rendering;
namespace Umbraco.Cms.Api.Delivery.Rendering;
internal sealed class RequestContextOutputExpansionStrategyV2 : IOutputExpansionStrategy
internal sealed class RequestContextOutputExpansionStrategyV2 : ElementOnlyOutputExpansionStrategy, IOutputExpansionStrategy
{
private const string All = "$all";
private const string None = "";
private const string ExpandParameterName = "expand";
private const string FieldsParameterName = "fields";
private readonly IApiPropertyRenderer _propertyRenderer;
private readonly ILogger<RequestContextOutputExpansionStrategyV2> _logger;
private readonly Stack<Node?> _expandProperties;
private readonly Stack<Node?> _includeProperties;
public RequestContextOutputExpansionStrategyV2(
IHttpContextAccessor httpContextAccessor,
IApiPropertyRenderer propertyRenderer,
ILogger<RequestContextOutputExpansionStrategyV2> logger)
: base(propertyRenderer)
{
_propertyRenderer = propertyRenderer;
_logger = logger;
_expandProperties = new Stack<Node?>();
_includeProperties = new Stack<Node?>();
InitializeExpandAndInclude(httpContextAccessor);
}
public IDictionary<string, object?> MapContentProperties(IPublishedContent content)
=> content.ItemType == PublishedItemType.Content
? MapProperties(content.Properties)
: throw new ArgumentException($"Invalid item type. This method can only be used with item type {nameof(PublishedItemType.Content)}, got: {content.ItemType}");
public IDictionary<string, object?> MapMediaProperties(IPublishedContent media, bool skipUmbracoProperties = true)
{
if (media.ItemType != PublishedItemType.Media)
{
throw new ArgumentException($"Invalid item type. This method can only be used with item type {PublishedItemType.Media}, got: {media.ItemType}");
}
IPublishedProperty[] properties = media
.Properties
.Where(p => skipUmbracoProperties is false || p.Alias.StartsWith("umbraco") is false)
.ToArray();
return properties.Any()
? MapProperties(properties)
: new Dictionary<string, object?>();
}
public IDictionary<string, object?> MapElementProperties(IPublishedElement element)
=> MapProperties(element.Properties, true);
private void InitializeExpandAndInclude(IHttpContextAccessor httpContextAccessor)
{
string? QueryValue(string key) => httpContextAccessor.HttpContext?.Request.Query[key];
@@ -66,7 +28,7 @@ internal sealed class RequestContextOutputExpansionStrategyV2 : IOutputExpansion
try
{
_expandProperties.Push(Node.Parse(toExpand));
ExpandProperties.Push(Node.Parse(toExpand));
}
catch (ArgumentException ex)
{
@@ -76,7 +38,7 @@ internal sealed class RequestContextOutputExpansionStrategyV2 : IOutputExpansion
try
{
_includeProperties.Push(Node.Parse(toInclude));
IncludeProperties.Push(Node.Parse(toInclude));
}
catch (ArgumentException ex)
{
@@ -84,102 +46,4 @@ internal sealed class RequestContextOutputExpansionStrategyV2 : IOutputExpansion
throw new ArgumentException($"Could not parse the '{FieldsParameterName}' parameter: {ex.Message}");
}
}
private IDictionary<string, object?> MapProperties(IEnumerable<IPublishedProperty> properties, bool forceExpandProperties = false)
{
Node? currentExpandProperties = _expandProperties.Peek();
if (_expandProperties.Count > 1 && currentExpandProperties is null && forceExpandProperties is false)
{
return new Dictionary<string, object?>();
}
Node? currentIncludeProperties = _includeProperties.Peek();
var result = new Dictionary<string, object?>();
foreach (IPublishedProperty property in properties)
{
Node? nextIncludeProperties = GetNextProperties(currentIncludeProperties, property.Alias);
if (currentIncludeProperties is not null && currentIncludeProperties.Items.Any() && nextIncludeProperties is null)
{
continue;
}
Node? nextExpandProperties = GetNextProperties(currentExpandProperties, property.Alias);
_includeProperties.Push(nextIncludeProperties);
_expandProperties.Push(nextExpandProperties);
result[property.Alias] = GetPropertyValue(property);
_expandProperties.Pop();
_includeProperties.Pop();
}
return result;
}
private Node? GetNextProperties(Node? currentProperties, string propertyAlias)
=> currentProperties?.Items.FirstOrDefault(i => i.Key == All)
?? currentProperties?.Items.FirstOrDefault(i => i.Key == "properties")?.Items.FirstOrDefault(i => i.Key == All || i.Key == propertyAlias);
private object? GetPropertyValue(IPublishedProperty property)
=> _propertyRenderer.GetPropertyValue(property, _expandProperties.Peek() is not null);
private class Node
{
public string Key { get; private set; } = string.Empty;
public List<Node> Items { get; } = new();
public static Node Parse(string value)
{
// verify that there are as many start brackets as there are end brackets
if (value.CountOccurrences("[") != value.CountOccurrences("]"))
{
throw new ArgumentException("Value did not contain an equal number of start and end brackets");
}
// verify that the value does not start with a start bracket
if (value.StartsWith("["))
{
throw new ArgumentException("Value cannot start with a bracket");
}
// verify that there are no empty brackets
if (value.Contains("[]"))
{
throw new ArgumentException("Value cannot contain empty brackets");
}
var stack = new Stack<Node>();
var root = new Node { Key = "root" };
stack.Push(root);
var currentNode = new Node();
root.Items.Add(currentNode);
foreach (char c in value)
{
switch (c)
{
case '[': // Start a new node, child of the current node
stack.Push(currentNode);
currentNode = new Node();
stack.Peek().Items.Add(currentNode);
break;
case ',': // Start a new node, but at the same level of the current node
currentNode = new Node();
stack.Peek().Items.Add(currentNode);
break;
case ']': // Back to parent of the current node
currentNode = stack.Pop();
break;
default: // Add char to current node key
currentNode.Key += c;
break;
}
}
return root;
}
}
}
@@ -1,4 +1,4 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Options;
using Umbraco.Cms.Core.Configuration.Models;
using Umbraco.Cms.Core.DeliveryApi;
@@ -29,7 +29,7 @@ internal sealed class ApiAccessService : RequestHeaderHandler, IApiAccessService
private bool IfEnabled(Func<bool> condition) => _deliveryApiSettings.Enabled && condition();
private bool HasValidApiKey() => _deliveryApiSettings.ApiKey.IsNullOrWhiteSpace() == false
&& _deliveryApiSettings.ApiKey.Equals(GetHeaderValue("Api-Key"));
&& _deliveryApiSettings.ApiKey.Equals(GetHeaderValue(Core.Constants.DeliveryApi.HeaderNames.ApiKey));
private bool IfMediaEnabled(Func<bool> condition) => _deliveryApiSettings is { Enabled: true, Media.Enabled: true } && condition();
}
@@ -11,5 +11,5 @@ internal sealed class RequestPreviewService : RequestHeaderHandler, IRequestPrev
}
/// <inheritdoc />
public bool IsPreview() => string.Equals(GetHeaderValue("Preview"), "true", StringComparison.OrdinalIgnoreCase);
public bool IsPreview() => string.Equals(GetHeaderValue(Core.Constants.DeliveryApi.HeaderNames.Preview), "true", StringComparison.OrdinalIgnoreCase);
}
@@ -58,5 +58,5 @@ internal sealed class RequestStartItemProvider : RequestHeaderHandler, IRequestS
}
/// <inheritdoc/>
public string? RequestedStartItem() => GetHeaderValue("Start-Item");
public string? RequestedStartItem() => GetHeaderValue(Constants.DeliveryApi.HeaderNames.StartItem);
}
@@ -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>
@@ -5,7 +5,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="SixLabors.ImageSharp" VersionOverride="[2.1.10, 3)" />
<PackageReference Include="SixLabors.ImageSharp" VersionOverride="[2.1.11, 3)" />
<PackageReference Include="SixLabors.ImageSharp.Web" VersionOverride="[2.0.2, 3)" />
</ItemGroup>
@@ -127,10 +127,16 @@ internal class EFCoreScope<TDbContext> : CoreScope, IEfCoreScope<TDbContext>
Locks.ClearLocks(InstanceId);
if (ParentScope is null)
// Since we can nest EFCoreScopes in other scopes derived from CoreScope, we should check whether our ParentScope OR the base ParentScope exists.
// Only if neither do do we take responsibility for ensuring the locks are cleared.
// Eventually the highest parent will clear the locks.
// Further, these locks are a reference to the locks of the highest parent anyway (see the constructor of CoreScope).
#pragma warning disable SA1100 // Do not prefix calls with base unless local implementation exists (justification: provides additional clarify here that this is defined on the base class).
if (ParentScope is null && base.HasParentScope is false)
{
Locks.EnsureLocksCleared(InstanceId);
}
#pragma warning restore SA1100 // Do not prefix calls with base unless local implementation exists
_efCoreScopeProvider.PopAmbientScope();
+20 -3
View File
@@ -43,24 +43,41 @@ public static class AppCacheExtensions
public static T? GetCacheItem<T>(this IAppCache provider, string cacheKey)
{
var result = provider.Get(cacheKey);
if (IsRetrievedItemNull(result))
if (result == null)
{
return default;
}
// If we've retrieved the specific string that represents null in the cache, return it only if we are requesting it (via a typed request for a string).
// Otherwise consider it a null value.
if (RetrievedNullRepresentationInCache(result))
{
return RequestedNullRepresentationInCache<T>() ? (T)result : default;
}
return result.TryConvertTo<T>().Result;
}
public static T? GetCacheItem<T>(this IAppCache provider, string cacheKey, Func<T> getCacheItem)
{
var result = provider.Get(cacheKey, () => getCacheItem());
if (IsRetrievedItemNull(result))
if (result == null)
{
return default;
}
// If we've retrieved the specific string that represents null in the cache, return it only if we are requesting it (via a typed request for a string).
// Otherwise consider it a null value.
if (RetrievedNullRepresentationInCache(result))
{
return RequestedNullRepresentationInCache<T>() ? (T)result : default;
}
return result.TryConvertTo<T>().Result;
}
private static bool IsRetrievedItemNull(object? result) => result is null or (object)Cms.Core.Constants.Cache.NullRepresentationInCache;
private static bool RetrievedNullRepresentationInCache(object result) => result == (object)Cms.Core.Constants.Cache.NullRepresentationInCache;
private static bool RequestedNullRepresentationInCache<T>() => typeof(T) == typeof(string);
}
@@ -159,15 +159,30 @@ public static class DistributedCacheExtensions
=> dc.RefreshMemberCache(members.AsEnumerable());
public static void RefreshMemberCache(this DistributedCache dc, IEnumerable<IMember> members)
=> dc.RefreshByPayload(MemberCacheRefresher.UniqueId, members.DistinctBy(x => (x.Id, x.Username)).Select(x => new MemberCacheRefresher.JsonPayload(x.Id, x.Username, false)));
=> dc.RefreshByPayload(
MemberCacheRefresher.UniqueId,
GetPayloads(members, false));
[Obsolete("Use the overload accepting IEnumerable instead to avoid allocating arrays. This overload will be removed in Umbraco 13.")]
public static void RemoveMemberCache(this DistributedCache dc, params IMember[] members)
=> dc.RemoveMemberCache(members.AsEnumerable());
public static void RemoveMemberCache(this DistributedCache dc, IEnumerable<IMember> members)
=> dc.RefreshByPayload(MemberCacheRefresher.UniqueId, members.DistinctBy(x => (x.Id, x.Username)).Select(x => new MemberCacheRefresher.JsonPayload(x.Id, x.Username, true)));
=> dc.RefreshByPayload(
MemberCacheRefresher.UniqueId,
GetPayloads(members, true));
// Internal for unit test.
internal static IEnumerable<MemberCacheRefresher.JsonPayload> GetPayloads(IEnumerable<IMember> members, bool removed)
=> members
.DistinctBy(x => (x.Id, x.Username))
.Select(x => new MemberCacheRefresher.JsonPayload(x.Id, x.Username, removed)
{
PreviousUsername = x.HasAdditionalData &&
x.AdditionalData!.TryGetValue(Cms.Core.Constants.Entities.AdditionalDataKeys.MemberPreviousUserName, out var previousUsername)
? previousUsername?.ToString()
: null,
});
#endregion
@@ -0,0 +1,9 @@
namespace Umbraco.Cms.Core.Cache.PartialViewCacheInvalidators;
internal class NoopMemberPartialViewCacheInvalidator : IMemberPartialViewCacheInvalidator
{
public void ClearPartialViewCacheItems(IEnumerable<int> memberIds)
{
// No operation performed, this is a no-op implementation.
}
}
@@ -70,6 +70,8 @@ public sealed class MemberCacheRefresher : PayloadCacheRefresherBase<MemberCache
public string? Username { get; }
public string? PreviousUsername { get; set; }
public bool Removed { get; }
}
@@ -121,6 +123,13 @@ public sealed class MemberCacheRefresher : PayloadCacheRefresherBase<MemberCache
// 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));
// If provided, clear the cache by the previous user name too.
if (string.IsNullOrEmpty(p.PreviousUsername) is false)
{
memberCache.Result?.Clear(RepositoryCacheKeys.GetKey<IMember, string>(p.PreviousUsername));
memberCache.Result?.Clear(RepositoryCacheKeys.GetKey<IMember, string>(CacheKeys.MemberUserNameCachePrefix + p.PreviousUsername));
}
}
}
}
+9 -1
View File
@@ -51,7 +51,15 @@ public class ValueEditorCache : IValueEditorCache
{
foreach (Dictionary<int, IDataValueEditor> editors in _valueEditorCache.Values)
{
editors.Remove(id);
if (editors.TryGetValue(id, out IDataValueEditor? editor))
{
if (editor is IDisposable disposable)
{
disposable.Dispose();
}
editors.Remove(id);
}
}
}
}
+31 -3
View File
@@ -1,4 +1,4 @@
namespace Umbraco.Cms.Core;
namespace Umbraco.Cms.Core;
public static partial class Constants
{
@@ -24,14 +24,42 @@ public static partial class Constants
public static class OutputCache
{
/// <summary>
/// Output cache policy name for content
/// Output cache policy name for content.
/// </summary>
public const string ContentCachePolicy = "DeliveryApiContent";
/// <summary>
/// Output cache policy name for media
/// Output cache policy name for media.
/// </summary>
public const string MediaCachePolicy = "DeliveryApiMedia";
}
/// <summary>
/// Constants for Delivery API header names.
/// </summary>
public static class HeaderNames
{
/// <summary>
/// Header name for accept language.
/// </summary>
public const string AcceptLanguage = "Accept-Language";
/// <summary>
/// Header name for API key.
/// </summary>
public const string ApiKey = "Api-Key";
/// <summary>
/// Header name for preview.
/// </summary>
public const string Preview = "Preview";
/// <summary>
/// Header name for start item.
/// </summary>
public const string StartItem = "Start-Item";
}
}
}
+14
View File
@@ -0,0 +1,14 @@
namespace Umbraco.Cms.Core;
public static partial class Constants
{
public static class Entities
{
public static class AdditionalDataKeys
{
public const string MemberPreviousUserName = "previousUsername";
public const string MemberGroupPreviousName = "previousName";
}
}
}
@@ -8,12 +8,14 @@ using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using Umbraco.Cms.Core.Cache;
using Umbraco.Cms.Core.Cache.PartialViewCacheInvalidators;
using Umbraco.Cms.Core.Composing;
using Umbraco.Cms.Core.Configuration;
using Umbraco.Cms.Core.Configuration.Grid;
using Umbraco.Cms.Core.Configuration.Models;
using Umbraco.Cms.Core.Diagnostics;
using Umbraco.Cms.Core.Dictionary;
using Umbraco.Cms.Core.DynamicRoot;
using Umbraco.Cms.Core.Editors;
using Umbraco.Cms.Core.Events;
using Umbraco.Cms.Core.Features;
@@ -35,7 +37,6 @@ using Umbraco.Cms.Core.Runtime;
using Umbraco.Cms.Core.Scoping;
using Umbraco.Cms.Core.Security;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.DynamicRoot;
using Umbraco.Cms.Core.Sync;
using Umbraco.Cms.Core.Telemetry;
using Umbraco.Cms.Core.Templates;
@@ -341,6 +342,11 @@ namespace Umbraco.Cms.Core.DependencyInjection
// Data type configuration cache
Services.AddUnique<IDataTypeConfigurationCache, DataTypeConfigurationCache>();
Services.AddNotificationHandler<DataTypeCacheRefresherNotification, DataTypeConfigurationCacheRefresher>();
// Partial view cache invalidators (no-op, shipped implementation is added in Umbraco.Web.Website, but we
// need this to ensure we have a service registered for this interface even in headless setups).
// See: https://github.com/umbraco/Umbraco-CMS/issues/19661
Services.AddUnique<IMemberPartialViewCacheInvalidator, NoopMemberPartialViewCacheInvalidator>();
}
}
}
File diff suppressed because it is too large Load Diff
@@ -826,7 +826,7 @@ public static class PublishedContentExtensions
/// </param>
/// <returns></returns>
/// <remarks>
/// This can be useful in order to return all nodes in an entire site by a type when combined with TypedContentAtRoot
/// This can be useful in order to return all nodes in an entire site by a type when combined with ContentAtRoot
/// </remarks>
public static IEnumerable<IPublishedContent> DescendantsOrSelfOfType(
this IEnumerable<IPublishedContent> parentNodes, IVariationContextAccessor variationContextAccessor, string docTypeAlias, string? culture = null) => parentNodes.SelectMany(x =>
@@ -843,7 +843,7 @@ public static class PublishedContentExtensions
/// </param>
/// <returns></returns>
/// <remarks>
/// This can be useful in order to return all nodes in an entire site by a type when combined with TypedContentAtRoot
/// This can be useful in order to return all nodes in an entire site by a type when combined with ContentAtRoot
/// </remarks>
public static IEnumerable<T> DescendantsOrSelf<T>(this IEnumerable<IPublishedContent> parentNodes, IVariationContextAccessor variationContextAccessor, string? culture = null)
where T : class, IPublishedContent =>
@@ -21,16 +21,17 @@ public sealed class PublicAccessHandler :
private void Handle(IEnumerable<IMemberGroup> affectedEntities)
{
var keyName = Constants.Entities.AdditionalDataKeys.MemberGroupPreviousName;
foreach (IMemberGroup grp in affectedEntities)
{
// check if the name has changed
if ((grp.AdditionalData?.ContainsKey("previousName") ?? false)
&& grp.AdditionalData["previousName"] != null
&& grp.AdditionalData["previousName"]?.ToString().IsNullOrWhiteSpace() == false
&& grp.AdditionalData["previousName"]?.ToString() != grp.Name)
if ((grp.AdditionalData?.ContainsKey(keyName) ?? false)
&& grp.AdditionalData[keyName] != null
&& grp.AdditionalData[keyName]?.ToString().IsNullOrWhiteSpace() == false
&& grp.AdditionalData[keyName]?.ToString() != grp.Name)
{
_publicAccessService.RenameMemberGroupRoleRules(
grp.AdditionalData["previousName"]?.ToString(),
grp.AdditionalData[keyName]?.ToString(),
grp.Name);
}
}
@@ -346,13 +346,6 @@ namespace Umbraco.Cms.Core.IO
// our root path, due to relative segments, so better check
if (_ioHelper.PathStartsWith(path, _rootPath, Path.DirectorySeparatorChar))
{
// this says that 4.7.2 supports long paths - but Windows does not
// https://docs.microsoft.com/en-us/dotnet/api/system.io.pathtoolongexception?view=netframework-4.7.2
if (path.Length > 260)
{
throw new PathTooLongException($"Path {path} is too long.");
}
return path;
}
@@ -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!;
+30 -5
View File
@@ -132,6 +132,30 @@ public abstract class ContentTypeBase : TreeEntityBase, IContentTypeBase
}
}
/// <summary>
/// Detects if any persisted property types have been removed by comparing old and new collections,
/// and sets <see cref="HasPropertyTypeBeenRemoved"/> accordingly.
/// </summary>
private void DetectPropertyTypeRemovals(IEnumerable<IPropertyType> oldPropertyTypes, IEnumerable<IPropertyType> newPropertyTypes)
{
if (HasPropertyTypeBeenRemoved)
{
return;
}
var oldIds = new HashSet<int>(oldPropertyTypes.Select(pt => pt.Id).Where(id => id > 0));
if (oldIds.Count == 0)
{
return;
}
var newIds = new HashSet<int>(newPropertyTypes.Select(pt => pt.Id).Where(id => id > 0));
if (oldIds.Any(id => !newIds.Contains(id)))
{
HasPropertyTypeBeenRemoved = true;
}
}
/// <summary>
/// PropertyTypes that are not part of a PropertyGroup
/// </summary>
@@ -273,6 +297,7 @@ public abstract class ContentTypeBase : TreeEntityBase, IContentTypeBase
{
if (PropertyTypeCollection != null)
{
DetectPropertyTypeRemovals(PropertyTypeCollection, value);
PropertyTypeCollection.ClearCollectionChangedEvents();
}
@@ -443,21 +468,21 @@ public abstract class ContentTypeBase : TreeEntityBase, IContentTypeBase
/// Please note that resetting the dirty properties could potentially
/// obstruct the saving of a new or updated entity.
/// </remarks>
public override void ResetDirtyProperties()
public override void ResetDirtyProperties(bool rememberDirty)
{
base.ResetDirtyProperties();
base.ResetDirtyProperties(rememberDirty);
// loop through each property group to reset the property types
var propertiesReset = new List<int>();
foreach (PropertyGroup propertyGroup in PropertyGroups)
{
propertyGroup.ResetDirtyProperties();
propertyGroup.ResetDirtyProperties(rememberDirty);
if (propertyGroup.PropertyTypes is not null)
{
foreach (IPropertyType propertyType in propertyGroup.PropertyTypes)
{
propertyType.ResetDirtyProperties();
propertyType.ResetDirtyProperties(rememberDirty);
propertiesReset.Add(propertyType.Id);
}
}
@@ -467,7 +492,7 @@ public abstract class ContentTypeBase : TreeEntityBase, IContentTypeBase
// but don't re-reset ones we've already done.
foreach (IPropertyType propertyType in PropertyTypes.Where(x => propertiesReset.Contains(x.Id) == false))
{
propertyType.ResetDirtyProperties();
propertyType.ResetDirtyProperties(rememberDirty);
}
}
@@ -13,6 +13,7 @@ public abstract class ContentTypeCompositionBase : ContentTypeBase, IContentType
{
private List<IContentTypeComposition> _contentTypeComposition = new();
private List<int> _removedContentTypeKeyTracker = new();
private bool _hasCompositionBeenRemoved;
protected ContentTypeCompositionBase(IShortStringHelper shortStringHelper, int parentId)
: base(shortStringHelper, parentId)
@@ -104,6 +105,24 @@ public abstract class ContentTypeCompositionBase : ContentTypeBase, IContentType
}
}
/// <summary>
/// A boolean flag indicating if a composition has been removed from this instance.
/// </summary>
/// <remarks>
/// This is currently (specifically) used in order to know that we need to refresh the content cache which
/// needs to occur when a composition has been removed from a content type
/// </remarks>
[IgnoreDataMember]
internal bool HasCompositionTypeBeenRemoved
{
get => _hasCompositionBeenRemoved;
private set
{
_hasCompositionBeenRemoved = value;
OnPropertyChanged(nameof(HasCompositionTypeBeenRemoved));
}
}
/// <inheritdoc />
public IEnumerable<IPropertyType> GetOriginalComposedPropertyTypes() => GetRawComposedPropertyTypes();
@@ -179,6 +198,8 @@ public abstract class ContentTypeCompositionBase : ContentTypeBase, IContentType
_removedContentTypeKeyTracker.AddRange(compositionIdsToRemove);
}
HasCompositionTypeBeenRemoved = true;
OnPropertyChanged(nameof(ContentTypeComposition));
return _contentTypeComposition.Remove(contentTypeComposition);
@@ -10,5 +10,7 @@ public sealed class ApiContentRoute : IApiContentRoute
public string Path { get; }
public string? QueryString { get; set; }
public IApiContentStartItem StartItem { get; }
}
@@ -4,5 +4,10 @@ public interface IApiContentRoute
{
string Path { get; }
public string? QueryString
{
get => null; set { }
}
IApiContentStartItem StartItem { get; }
}
+1 -1
View File
@@ -35,7 +35,7 @@ _additionalData ??= new Dictionary<string, object?>();
// if the name has changed, add the value to the additional data,
// this is required purely for event handlers to know the previous name of the group
// so we can keep the public access up to date.
AdditionalData["previousName"] = _name;
AdditionalData[Constants.Entities.AdditionalDataKeys.MemberGroupPreviousName] = _name;
}
SetPropertyValueAndDetectChanges(value, ref _name, nameof(Name));
+39
View File
@@ -20,6 +20,7 @@ public class PropertyGroup : EntityBase, IEquatable<PropertyGroup>
private string _alias;
private string? _name;
private bool _hasPropertyTypeBeenRemoved;
private PropertyTypeCollection? _propertyTypes;
private int _sortOrder;
@@ -94,6 +95,20 @@ public class PropertyGroup : EntityBase, IEquatable<PropertyGroup>
set => SetPropertyValueAndDetectChanges(value, ref _sortOrder, nameof(SortOrder));
}
/// <summary>
/// A boolean flag indicating if a property type has been removed from this group.
/// </summary>
[IgnoreDataMember]
internal bool HasPropertyTypeBeenRemoved
{
get => _hasPropertyTypeBeenRemoved;
private set
{
_hasPropertyTypeBeenRemoved = value;
OnPropertyChanged(nameof(HasPropertyTypeBeenRemoved));
}
}
/// <summary>
/// Gets or sets a collection of property types for the group.
/// </summary>
@@ -112,6 +127,7 @@ public class PropertyGroup : EntityBase, IEquatable<PropertyGroup>
{
if (_propertyTypes != null)
{
DetectPropertyTypeRemovals(_propertyTypes, value);
_propertyTypes.ClearCollectionChangedEvents();
}
@@ -155,4 +171,27 @@ public class PropertyGroup : EntityBase, IEquatable<PropertyGroup>
private void PropertyTypesChanged(object? sender, NotifyCollectionChangedEventArgs e) =>
OnPropertyChanged(nameof(PropertyTypes));
private void DetectPropertyTypeRemovals(PropertyTypeCollection oldPropertyTypes, PropertyTypeCollection? newPropertyTypes)
{
if (HasPropertyTypeBeenRemoved)
{
return;
}
var oldIds = new HashSet<int>(oldPropertyTypes.Select(pt => pt.Id).Where(id => id > 0));
if (oldIds.Count == 0)
{
return;
}
var newIds = newPropertyTypes != null
? new HashSet<int>(newPropertyTypes.Select(pt => pt.Id).Where(id => id > 0))
: new HashSet<int>();
if (oldIds.Any(id => !newIds.Contains(id)))
{
HasPropertyTypeBeenRemoved = true;
}
}
}
@@ -30,7 +30,7 @@ public class PublishedValueFallback : IPublishedValueFallback
/// <inheritdoc />
public bool TryGetValue<T>(IPublishedProperty property, string? culture, string? segment, Fallback fallback, T? defaultValue, out T? value)
{
_variationContextAccessor.ContextualizeVariation(property.PropertyType.Variations, ref culture, ref segment);
_variationContextAccessor.ContextualizeVariation(property.PropertyType.Variations, property.Alias, ref culture, ref segment);
foreach (var f in fallback)
{
@@ -78,7 +78,7 @@ public class PublishedValueFallback : IPublishedValueFallback
return false;
}
_variationContextAccessor.ContextualizeVariation(propertyType.Variations, ref culture, ref segment);
_variationContextAccessor.ContextualizeVariation(propertyType.Variations, alias, ref culture, ref segment);
foreach (var f in fallback)
{
@@ -124,7 +124,7 @@ public class PublishedValueFallback : IPublishedValueFallback
IPublishedPropertyType? propertyType = content.ContentType.GetPropertyType(alias);
if (propertyType != null)
{
_variationContextAccessor.ContextualizeVariation(propertyType.Variations, content.Id, ref culture, ref segment);
_variationContextAccessor.ContextualizeVariation(propertyType.Variations, content.Id, alias, ref culture, ref segment);
noValueProperty = content.GetProperty(alias);
}
@@ -195,7 +195,7 @@ public class PublishedValueFallback : IPublishedValueFallback
{
culture = null;
segment = null;
_variationContextAccessor.ContextualizeVariation(propertyType.Variations, content.Id, ref culture, ref segment);
_variationContextAccessor.ContextualizeVariation(propertyType.Variations, content.Id, alias, ref culture, ref segment);
}
property = content?.GetProperty(alias);
@@ -25,9 +25,15 @@ public class VariationContext
public string Segment { get; }
/// <summary>
/// Gets the segment for the content item
/// Gets the segment for the content item.
/// </summary>
/// <param name="contentId"></param>
/// <returns></returns>
/// <param name="contentId">The content Id.</param>
public virtual string GetSegment(int contentId) => Segment;
/// <summary>
/// Gets the segment for the content item and property alias.
/// </summary>
/// <param name="contentId">The content Id.</param>
/// <param name="propertyAlias">The property alias.</param>
public virtual string GetSegment(int contentId, string propertyAlias) => Segment;
}
@@ -8,25 +8,45 @@ namespace Umbraco.Extensions;
public static class VariationContextAccessorExtensions
{
[Obsolete("Please use the method overload that accepts all parameters. Scheduled for removal in Umbraco 18.")]
public static void ContextualizeVariation(
this IVariationContextAccessor variationContextAccessor,
ContentVariation variations,
ref string? culture,
ref string? segment)
=> variationContextAccessor.ContextualizeVariation(variations, null, ref culture, ref segment);
=> variationContextAccessor.ContextualizeVariation(variations, null, null, ref culture, ref segment);
public static void ContextualizeVariation(
this IVariationContextAccessor variationContextAccessor,
ContentVariation variations,
string? propertyAlias,
ref string? culture,
ref string? segment)
=> variationContextAccessor.ContextualizeVariation(variations, null, propertyAlias, ref culture, ref segment);
[Obsolete("Please use the method overload that accepts all parameters. Scheduled for removal in Umbraco 18.")]
public static void ContextualizeVariation(
this IVariationContextAccessor variationContextAccessor,
ContentVariation variations,
int contentId,
ref string? culture,
ref string? segment)
=> variationContextAccessor.ContextualizeVariation(variations, (int?)contentId, ref culture, ref segment);
=> variationContextAccessor.ContextualizeVariation(variations, (int?)contentId, null, ref culture, ref segment);
public static void ContextualizeVariation(
this IVariationContextAccessor variationContextAccessor,
ContentVariation variations,
int contentId,
string? propertyAlias,
ref string? culture,
ref string? segment)
=> variationContextAccessor.ContextualizeVariation(variations, (int?)contentId, propertyAlias, ref culture, ref segment);
private static void ContextualizeVariation(
this IVariationContextAccessor variationContextAccessor,
ContentVariation variations,
int? contentId,
string? propertyAlias,
ref string? culture,
ref string? segment)
{
@@ -37,18 +57,22 @@ public static class VariationContextAccessorExtensions
// use context values
VariationContext? publishedVariationContext = variationContextAccessor?.VariationContext;
if (culture == null)
{
culture = variations.VariesByCulture() ? publishedVariationContext?.Culture : string.Empty;
}
culture ??= variations.VariesByCulture() ? publishedVariationContext?.Culture : string.Empty;
if (segment == null)
{
if (variations.VariesBySegment())
{
segment = contentId == null
? publishedVariationContext?.Segment
: publishedVariationContext?.GetSegment(contentId.Value);
if (contentId == null)
{
segment = publishedVariationContext?.Segment;
}
else
{
segment = propertyAlias == null ?
publishedVariationContext?.GetSegment(contentId.Value) :
publishedVariationContext?.GetSegment(contentId.Value, propertyAlias);
}
}
else
{
+2
View File
@@ -250,6 +250,8 @@ public class CoreScope : ICoreScope
_parentScope = coreScope;
}
protected bool HasParentScope => _parentScope is not null;
protected void HandleScopedNotifications() => _notificationPublisher?.ScopeExit(Completed.HasValue && Completed.Value);
private void EnsureNotDisposed()
@@ -18,4 +18,20 @@ public static class ContentTypeChangeExtensions
public static bool HasTypesNone(this ContentTypeChangeTypes change, ContentTypeChangeTypes types) =>
(change & types) == ContentTypeChangeTypes.None;
/// <summary>
/// Determines whether the change has structural change impact.
/// </summary>
/// <param name="change">The change to check.</param>
/// <returns><c>true</c> if the change has structural impact; otherwise, <c>false</c>.</returns>
public static bool IsStructuralChange(this ContentTypeChangeTypes change) =>
change.HasType(ContentTypeChangeTypes.RefreshMain);
/// <summary>
/// Determines whether the change has non-structural change impact.
/// </summary>
/// <param name="change">The change to check.</param>
/// <returns><c>true</c> if the change has non-structural impact; otherwise, <c>false</c>.</returns>
public static bool IsNonStructuralChange(this ContentTypeChangeTypes change) =>
change.HasType(ContentTypeChangeTypes.RefreshOther) && !change.HasType(ContentTypeChangeTypes.RefreshMain);
}
@@ -11,14 +11,29 @@ public enum ContentTypeChangeTypes : byte
Create = 1,
/// <summary>
/// Content type changes impact only the Content type being saved
/// Content type changes directly impact existing content of this content type.
/// </summary>
/// <remarks>
/// These changes are "destructive" of nature. They include:
/// - Changing the content type alias.
/// - Removing a property type or a composition.
/// - Changing the alias of a property type (this effectively corresponds to removing a property type).
/// - Changing variance, either at property or content type level.
/// </remarks>
RefreshMain = 2,
/// <summary>
/// Content type changes impacts the content type being saved and others used that are composed of it
/// Content type changes that do not directly impact existing content of this content type.
/// </summary>
RefreshOther = 4, // changed, other change
/// <remarks>
/// These changes are "constructive" of nature, and include all changes not included in
/// <see cref="RefreshMain"/> - for example:
/// - Adding a property type or a composition.
/// - Rearranging property types or groups.
/// - Changes to name, description, icon etc.
/// - Changes to other content type settings, i.e. allowed child types and version cleanup.
/// </remarks>
RefreshOther = 4,
/// <summary>
/// Content type was removed
+2 -1
View File
@@ -2199,7 +2199,7 @@ public class ContentService : RepositoryService, IContentService
variesByCulture ? culturesPublished.IsCollectionEmpty() ? null : culturesPublished : ["*"],
null,
eventMessages));
scope.Notifications.Publish(new ContentPublishedNotification(publishedDocuments, eventMessages).WithState(notificationState));
scope.Notifications.Publish(new ContentPublishedNotification(publishedDocuments, eventMessages, true).WithState(notificationState));
scope.Complete();
}
@@ -3627,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)
@@ -226,7 +226,10 @@ public abstract class ContentTypeServiceBase<TRepository, TItem> : ContentTypeSe
});
// removed properties?
var hasAnyPropertyBeenRemoved = dirty.WasPropertyDirty("HasPropertyTypeBeenRemoved");
// check both the content type level flag (set by RemovePropertyType) and
// individual property group flags (set when PropertyTypes collection is replaced, e.g. by the mapper)
var hasAnyPropertyBeenRemoved = dirty.WasPropertyDirty("HasPropertyTypeBeenRemoved")
|| contentType.PropertyGroups.Any(g => g.WasPropertyDirty("HasPropertyTypeBeenRemoved"));
// removed compositions?
var hasAnyCompositionBeenRemoved = dirty.WasPropertyDirty("HasCompositionTypeBeenRemoved");
@@ -56,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>
+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);
}
+14 -3
View File
@@ -770,16 +770,27 @@ namespace Umbraco.Cms.Core.Services
throw new ArgumentException("Cannot save member with empty name.");
}
var previousUsername = _memberRepository.Get(member.Id)?.Username;
scope.WriteLock(Constants.Locks.MemberTree);
_memberRepository.Save(member);
if (publishNotificationSaveOptions.HasFlag(PublishNotificationSaveOptions.Saved))
{
scope.Notifications.Publish(
savingNotification is null
// If the user name has changed, populate the previous user name in the additional data, so the cache refreshers
// have it available to clear the cache by the old name as well as the new.
if (string.IsNullOrWhiteSpace(previousUsername) is false &&
string.Equals(previousUsername, member.Username, StringComparison.OrdinalIgnoreCase) is false)
{
member.AdditionalData![Constants.Entities.AdditionalDataKeys.MemberPreviousUserName] = previousUsername;
}
MemberSavedNotification memberSavedNotification = savingNotification is null
? new MemberSavedNotification(member, evtMsgs)
: new MemberSavedNotification(member, evtMsgs).WithStateFrom(savingNotification));
: new MemberSavedNotification(member, evtMsgs).WithStateFrom(savingNotification);
scope.Notifications.Publish(memberSavedNotification);
}
Audit(AuditType.Save, 0, member.Id);
@@ -385,48 +385,7 @@ public class NotificationService : INotificationService
// build summary
var summary = new StringBuilder();
if (content.ContentType.VariesByNothing())
{
if (!_contentSettings.Notifications.DisableHtmlEmail)
{
// create the HTML summary for invariant content
// list all of the property values like we used to
summary.Append("<table style=\"width: 100 %; \">");
foreach (IProperty p in content.Properties)
{
// TODO: doesn't take into account variants
var newText = p.GetValue() != null ? p.GetValue()?.ToString() : string.Empty;
var oldText = newText;
// check if something was changed and display the changes otherwise display the fields
if (oldDoc?.Properties.Contains(p.PropertyType.Alias) ?? false)
{
IProperty? oldProperty = oldDoc.Properties[p.PropertyType.Alias];
oldText = oldProperty?.GetValue() != null ? oldProperty.GetValue()?.ToString() : string.Empty;
// replace HTML with char equivalent
ReplaceHtmlSymbols(ref oldText);
ReplaceHtmlSymbols(ref newText);
}
// show the values
summary.Append("<tr>");
summary.Append(
"<th style='text-align: left; vertical-align: top; width: 25%;border-bottom: 1px solid #CCC'>");
summary.Append(p.PropertyType.Name);
summary.Append("</th>");
summary.Append("<td style='text-align: left; vertical-align: top;border-bottom: 1px solid #CCC'>");
summary.Append(newText);
summary.Append("</td>");
summary.Append("</tr>");
}
summary.Append("</table>");
}
}
else if (content.ContentType.VariesByCulture())
{
if (content.ContentType.VariesByCulture()) {
// it's variant, so detect what cultures have changed
if (!_contentSettings.Notifications.DisableHtmlEmail)
{
@@ -465,8 +424,43 @@ public class NotificationService : INotificationService
}
else
{
// not supported yet...
throw new NotSupportedException();
if (!_contentSettings.Notifications.DisableHtmlEmail)
{
// create the HTML summary for invariant content
// list all of the property values like we used to
summary.Append("<table style=\"width: 100 %; \">");
foreach (IProperty p in content.Properties)
{
// TODO: doesn't take into account variants
var newText = p.GetValue() != null ? p.GetValue()?.ToString() : string.Empty;
var oldText = newText;
// check if something was changed and display the changes otherwise display the fields
if (oldDoc?.Properties.Contains(p.PropertyType.Alias) ?? false)
{
IProperty? oldProperty = oldDoc.Properties[p.PropertyType.Alias];
oldText = oldProperty?.GetValue() != null ? oldProperty.GetValue()?.ToString() : string.Empty;
// replace HTML with char equivalent
ReplaceHtmlSymbols(ref oldText);
ReplaceHtmlSymbols(ref newText);
}
// show the values
summary.Append("<tr>");
summary.Append(
"<th style='text-align: left; vertical-align: top; width: 25%;border-bottom: 1px solid #CCC'>");
summary.Append(p.PropertyType.Name);
summary.Append("</th>");
summary.Append("<td style='text-align: left; vertical-align: top;border-bottom: 1px solid #CCC'>");
summary.Append(newText);
summary.Append("</td>");
summary.Append("</tr>");
}
summary.Append("</table>");
}
}
var protocol = _globalSettings.UseHttps ? "https" : "http";
@@ -1,5 +1,4 @@
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging;
using Umbraco.Cms.Core.Events;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Persistence.Repositories;
@@ -146,5 +145,5 @@ internal class RedirectUrlService : RepositoryService, IRedirectUrlService
{
return await _redirectUrlRepository.GetMostRecentUrlAsync(url, culture);
}
}
}
}
@@ -1,5 +1,6 @@
using System.Globalization;
using System.Text.RegularExpressions;
using Umbraco.Cms.Core.Models.PublishedContent;
using Umbraco.Cms.Core.Routing;
using Umbraco.Cms.Core.Web;
@@ -43,7 +44,17 @@ public sealed class HtmlLocalLinkParser
/// <param name="text"></param>
/// <param name="preview"></param>
/// <returns></returns>
public string EnsureInternalLinks(string text, bool preview)
public string EnsureInternalLinks(string text, bool preview) =>
EnsureInternalLinks(text, preview, UrlMode.Default);
/// <summary>
/// Parses the string looking for the {localLink} syntax and updates them to their correct links.
/// </summary>
/// <param name="text"></param>
/// <param name="preview"></param>
/// <param name="urlMode"></param>
/// <returns></returns>
public string EnsureInternalLinks(string text, bool preview, UrlMode urlMode)
{
if (!_umbracoContextAccessor.TryGetUmbracoContext(out IUmbracoContext? umbracoContext))
{
@@ -52,12 +63,12 @@ public sealed class HtmlLocalLinkParser
if (!preview)
{
return EnsureInternalLinks(text);
return EnsureInternalLinks(text, urlMode);
}
using (umbracoContext.ForcedPreview(preview)) // force for URL provider
{
return EnsureInternalLinks(text);
return EnsureInternalLinks(text, urlMode);
}
}
@@ -66,7 +77,16 @@ public sealed class HtmlLocalLinkParser
/// </summary>
/// <param name="text"></param>
/// <returns></returns>
public string EnsureInternalLinks(string text)
public string EnsureInternalLinks(string text) =>
EnsureInternalLinks(text, UrlMode.Default);
/// <summary>
/// Parses the string looking for the {localLink} syntax and updates them to their correct links.
/// </summary>
/// <param name="text"></param>
/// <param name="urlMode"></param>
/// <returns></returns>
public string EnsureInternalLinks(string text, UrlMode urlMode)
{
if (!_umbracoContextAccessor.TryGetUmbracoContext(out _))
{
@@ -80,11 +100,11 @@ public sealed class HtmlLocalLinkParser
var newLink = "#";
if (udi?.EntityType == Constants.UdiEntityType.Document)
{
newLink = _publishedUrlProvider.GetUrl(udi.Guid);
newLink = _publishedUrlProvider.GetUrl(udi.Guid, urlMode);
}
else if (udi?.EntityType == Constants.UdiEntityType.Media)
{
newLink = _publishedUrlProvider.GetMediaUrl(udi.Guid);
newLink = _publishedUrlProvider.GetMediaUrl(udi.Guid, urlMode);
}
if (newLink == null)
@@ -96,7 +116,7 @@ public sealed class HtmlLocalLinkParser
}
else if (intId.HasValue)
{
var newLink = _publishedUrlProvider.GetUrl(intId.Value);
var newLink = _publishedUrlProvider.GetUrl(intId.Value, urlMode);
text = text.Replace(tagValue, "href=\"" + newLink);
}
}
@@ -28,9 +28,7 @@ public class BackOfficeExamineSearcher : IBackOfficeExamineSearcher
private readonly IEntityService _entityService;
private readonly IExamineManager _examineManager;
private readonly ILocalizationService _languageService;
private readonly IPublishedUrlProvider _publishedUrlProvider;
private readonly IUmbracoTreeSearcherFields _treeSearcherFields;
private readonly IUmbracoMapper _umbracoMapper;
public BackOfficeExamineSearcher(
IExamineManager examineManager,
@@ -48,8 +46,6 @@ public class BackOfficeExamineSearcher : IBackOfficeExamineSearcher
_entityService = entityService;
_treeSearcherFields = treeSearcherFields;
_appCaches = appCaches;
_umbracoMapper = umbracoMapper;
_publishedUrlProvider = publishedUrlProvider;
}
public IEnumerable<ISearchResult> Search(
@@ -82,8 +78,6 @@ public class BackOfficeExamineSearcher : IBackOfficeExamineSearcher
query = "\"" + g + "\"";
}
IUser? currentUser = _backOfficeSecurityAccessor?.BackOfficeSecurity?.CurrentUser;
switch (entityType)
{
case UmbracoEntityTypes.Member:
@@ -96,7 +90,7 @@ public class BackOfficeExamineSearcher : IBackOfficeExamineSearcher
}
if (searchFrom != null && searchFrom != Constants.Conventions.MemberTypes.AllMembersListId &&
searchFrom.Trim() != "-1")
searchFrom.Trim() != Constants.System.RootString)
{
sb.Append("+__NodeTypeAlias:");
sb.Append(searchFrom);
@@ -112,10 +106,13 @@ public class BackOfficeExamineSearcher : IBackOfficeExamineSearcher
fieldsToLoad.Add(field);
}
var allMediaStartNodes = currentUser != null
? currentUser.CalculateMediaStartNodeIds(_entityService, _appCaches)
: Array.Empty<int>();
AppendPath(sb, UmbracoObjectTypes.Media, allMediaStartNodes, searchFrom, ignoreUserStartNodes, _entityService);
AppendPath(sb, UmbracoObjectTypes.Media, searchFrom, ignoreUserStartNodes, out var abortMediaQuery);
if (abortMediaQuery)
{
totalFound = 0;
return [];
}
break;
case UmbracoEntityTypes.Document:
type = "content";
@@ -125,10 +122,13 @@ public class BackOfficeExamineSearcher : IBackOfficeExamineSearcher
fieldsToLoad.Add(field);
}
var allContentStartNodes = currentUser != null
? currentUser.CalculateContentStartNodeIds(_entityService, _appCaches)
: Array.Empty<int>();
AppendPath(sb, UmbracoObjectTypes.Document, allContentStartNodes, searchFrom, ignoreUserStartNodes, _entityService);
AppendPath(sb, UmbracoObjectTypes.Document, searchFrom, ignoreUserStartNodes, out var abortContentQuery);
if (abortContentQuery)
{
totalFound = 0;
return [];
}
break;
default:
throw new NotSupportedException("The " + typeof(BackOfficeExamineSearcher) +
@@ -344,67 +344,89 @@ public class BackOfficeExamineSearcher : IBackOfficeExamineSearcher
}
}
private void AppendPath(StringBuilder sb, UmbracoObjectTypes objectType, int[]? startNodeIds, string? searchFrom, bool ignoreUserStartNodes, IEntityService entityService)
private void AppendPath(StringBuilder sb, UmbracoObjectTypes objectType, string? searchFrom, bool ignoreUserStartNodes, out bool abortQuery)
{
if (sb == null)
ArgumentNullException.ThrowIfNull(sb);
abortQuery = false;
if (searchFrom is Constants.System.RootString)
{
throw new ArgumentNullException(nameof(sb));
searchFrom = null;
}
if (entityService == null)
var userStartNodes = ignoreUserStartNodes ? [Constants.System.Root] : GetUserStartNodes(objectType);
if (searchFrom is null && userStartNodes.Contains(Constants.System.Root))
{
throw new ArgumentNullException(nameof(entityService));
// If we have no searchFrom and the user either has access to the root node or we are ignoring user
// start nodes, we don't need to filter by path.
return;
}
UdiParser.TryParse(searchFrom, true, out Udi? udi);
searchFrom = udi == null ? searchFrom : entityService.GetId(udi).Result.ToString();
TreeEntityPath? entityPath =
int.TryParse(searchFrom, NumberStyles.Integer, CultureInfo.InvariantCulture, out var searchFromId) &&
searchFromId > 0
? entityService.GetAllPaths(objectType, searchFromId).FirstOrDefault()
: null;
if (entityPath != null)
string[] pathsToFilter;
if (searchFrom is null)
{
// find... only what's underneath
sb.Append("+__Path:");
AppendPath(sb, entityPath.Path, false);
sb.Append(" ");
// If we don't want to filter by a specific entity, we can simply use the user start nodes.
pathsToFilter = GetEntityPaths(objectType, userStartNodes);
}
else if (startNodeIds?.Length == 0)
else
{
// make sure we don't find anything
sb.Append("+__Path:none ");
}
else if (startNodeIds?.Contains(-1) == false && ignoreUserStartNodes == false) // -1 = no restriction
{
IEnumerable<TreeEntityPath> entityPaths = entityService.GetAllPaths(objectType, startNodeIds);
// for each start node, find the start node, and what's underneath
// +__Path:(-1*,1234 -1*,1234,* -1*,5678 -1*,5678,* ...)
sb.Append("+__Path:(");
var first = true;
foreach (TreeEntityPath ep in entityPaths)
TreeEntityPath? searchFromPath = GetEntityPath(searchFrom, objectType);
if (searchFromPath is null)
{
if (first)
{
first = false;
}
else
{
sb.Append(" ");
}
AppendPath(sb, ep.Path, true);
// If the searchFrom cannot be found, return no results.
// This is to prevent showing entities outside the intended filter.
abortQuery = true;
return;
}
sb.Append(") ");
var userStartNodePaths = GetEntityPaths(objectType, userStartNodes);
// If the user has access to the entity, we can simply filter by the entity path.
if (userStartNodePaths.Any(userStartNodePath => StartsWithPath(searchFromPath.Path, userStartNodePath)))
{
sb.Append("+__Path:");
AppendPath(sb, searchFromPath.Path, false);
sb.Append(' ');
return;
}
// If the user does not have access to the entity, let's filter the paths by the ones that start with the
// entity path (are descendants of the entity).
pathsToFilter = userStartNodePaths.Where(ep => StartsWithPath(ep, searchFromPath.Path)).ToArray();
}
// If we have no paths left, no need to perform the query at all, just return no results.
if (pathsToFilter.Length == 0)
{
abortQuery = true;
return;
}
// For each start node, find the start node, and what's underneath
// +__Path:(-1*,1234 -1*,1234,* -1*,5678 -1*,5678,* ...)
sb.Append("+__Path:(");
var first = true;
foreach (string pathToFilter in pathsToFilter)
{
if (first)
{
first = false;
}
else
{
sb.Append(' ');
}
AppendPath(sb, pathToFilter, true);
}
sb.Append(") ");
}
private void AppendPath(StringBuilder sb, string path, bool includeThisNode)
private static void AppendPath(StringBuilder sb, string path, bool includeThisNode)
{
path = path.Replace("-", "\\-").Replace(",", "\\,");
path = path.Replace("-", "\\-");
if (includeThisNode)
{
sb.Append(path);
@@ -412,6 +434,68 @@ public class BackOfficeExamineSearcher : IBackOfficeExamineSearcher
}
sb.Append(path);
sb.Append("\\,*");
sb.Append(",*");
}
private static bool StartsWithPath(string path1, string path2)
{
if (path1.StartsWith(path2) == false)
{
return false;
}
return path1.Length == path2.Length || path1[path2.Length] == ',';
}
private int[] GetUserStartNodes(UmbracoObjectTypes objectType)
{
IUser? currentUser = _backOfficeSecurityAccessor.BackOfficeSecurity?.CurrentUser;
if (currentUser is null)
{
return [];
}
var startNodes = objectType switch
{
UmbracoObjectTypes.Document => currentUser.CalculateContentStartNodeIds(_entityService, _appCaches),
UmbracoObjectTypes.Media => currentUser.CalculateMediaStartNodeIds(_entityService, _appCaches),
_ => throw new NotSupportedException($"The object type {objectType} is not supported for start nodes."),
};
return startNodes ?? [Constants.System.Root]; // If no start nodes are defined, we assume the user has access to the root node (-1).
}
private string[] GetEntityPaths(UmbracoObjectTypes objectType, int[] entityIds) =>
entityIds switch
{
[] => [],
_ when entityIds.Contains(Constants.System.Root) => [Constants.System.RootString],
_ => _entityService.GetAllPaths(objectType, entityIds).Select(x => x.Path).ToArray(),
};
private TreeEntityPath? GetEntityPath(string? searchFrom, UmbracoObjectTypes objectType)
{
if (searchFrom is null)
{
return null;
}
Guid? entityKey = null;
if (Guid.TryParse(searchFrom, out Guid entityGuid))
{
entityKey = entityGuid;
} // fallback to Udi for legacy reasons as the calling methods take string?
else if (UdiParser.TryParse(searchFrom, true, out Udi? udi) && udi is GuidUdi guidUdi)
{
entityKey = guidUdi.Guid;
}
else if (int.TryParse(searchFrom, NumberStyles.Integer, CultureInfo.InvariantCulture, out var entityId)
&& entityId > 0
&& _entityService.GetKey(entityId, objectType) is { Success: true } attempt)
{
entityKey = attempt.Result;
}
return entityKey is null ? null : _entityService.GetAllPaths(objectType, entityKey.Value).FirstOrDefault();
}
}
@@ -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,9 +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)
// - 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 || c.NextSibling is not null || string.IsNullOrWhiteSpace(c.InnerText) is false))
.Where(c => c.Name != CommentNodeName && (c.Name != TextNodeName || IsNonEmptyElement(c)))
.ToArray();
var tag = TagName(element);
@@ -124,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)
@@ -56,7 +56,7 @@ internal sealed class ApiRichTextMarkupParser : ApiRichTextParserBase, IApiRichT
link.GetAttributeValue("href", string.Empty),
route =>
{
link.SetAttributeValue("href", route.Path);
link.SetAttributeValue("href", $"{route.Path}{route.QueryString}");
link.SetAttributeValue("data-start-item-path", route.StartItem.Path);
link.SetAttributeValue("data-start-item-id", route.StartItem.Id.ToString("D"));
},
@@ -4,6 +4,7 @@ using Umbraco.Cms.Core.DeliveryApi;
using Umbraco.Cms.Core.Models.DeliveryApi;
using Umbraco.Cms.Core.Models.PublishedContent;
using Umbraco.Cms.Core.PublishedCache;
using Umbraco.Extensions;
namespace Umbraco.Cms.Infrastructure.DeliveryApi;
@@ -41,6 +42,7 @@ internal abstract partial class ApiRichTextParserBase
: null;
if (route != null)
{
route.QueryString = match.Groups["query"].Value.NullOrWhiteSpaceAsNull();
handled = true;
handleContentRoute(route);
}
@@ -79,6 +81,6 @@ internal abstract partial class ApiRichTextParserBase
handleMediaUrl(_apiMediaUrlProvider.GetUrl(media));
}
[GeneratedRegex("{localLink:(?<udi>umb:.+)}")]
[GeneratedRegex("{localLink:(?<udi>umb:.+)}(?<query>[^\"]*)")]
private static partial Regex LocalLinkRegex();
}
@@ -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);
}
}
@@ -35,6 +35,8 @@ public interface IPublishedContentQuery
IEnumerable<IPublishedContent> ContentAtRoot();
IEnumerable<IPublishedContent> ContentAtRoot(string? culture) => culture is null ? ContentAtRoot() : throw new NotSupportedException();
IPublishedContent? Media(int id);
IPublishedContent? Media(Guid id);
@@ -5,6 +5,7 @@ using Umbraco.Cms.Core.Cache;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Persistence.Querying;
using Umbraco.Cms.Core.Persistence.Repositories;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Strings;
using Umbraco.Cms.Infrastructure.Persistence.Dtos;
using Umbraco.Cms.Infrastructure.Persistence.Querying;
@@ -25,8 +26,9 @@ internal class ContentTypeRepository : ContentTypeRepositoryBase<IContentType>,
ILogger<ContentTypeRepository> logger,
IContentTypeCommonRepository commonRepository,
ILanguageRepository languageRepository,
IShortStringHelper shortStringHelper)
: base(scopeAccessor, cache, logger, commonRepository, languageRepository, shortStringHelper)
IShortStringHelper shortStringHelper,
Lazy<IIdKeyMap> idKeyMap)
: base(scopeAccessor, cache, logger, commonRepository, languageRepository, shortStringHelper, idKeyMap)
{
}
@@ -29,15 +29,22 @@ internal abstract class ContentTypeRepositoryBase<TEntity> : EntityRepositoryBas
where TEntity : class, IContentTypeComposition
{
private readonly IShortStringHelper _shortStringHelper;
private readonly Lazy<IIdKeyMap> _idKeyMap;
protected ContentTypeRepositoryBase(IScopeAccessor scopeAccessor, AppCaches cache,
ILogger<ContentTypeRepositoryBase<TEntity>> logger, IContentTypeCommonRepository commonRepository,
ILanguageRepository languageRepository, IShortStringHelper shortStringHelper)
protected ContentTypeRepositoryBase(
IScopeAccessor scopeAccessor,
AppCaches cache,
ILogger<ContentTypeRepositoryBase<TEntity>> logger,
IContentTypeCommonRepository commonRepository,
ILanguageRepository languageRepository,
IShortStringHelper shortStringHelper,
Lazy<IIdKeyMap> idKeyMap)
: base(scopeAccessor, cache, logger)
{
_shortStringHelper = shortStringHelper;
CommonRepository = commonRepository;
LanguageRepository = languageRepository;
_idKeyMap = idKeyMap;
}
protected IContentTypeCommonRepository CommonRepository { get; }
@@ -287,7 +294,7 @@ AND umbracoNode.nodeObjectType = @objectType",
// If the Id of the DataType is not set, we resolve it from the db by its PropertyEditorAlias
if (propertyType.DataTypeId == 0 || propertyType.DataTypeId == default)
{
AssignDataTypeFromPropertyEditor(propertyType);
AssignDataTypeIdFromProvidedKeyOrPropertyEditor(propertyType);
}
PropertyTypeDto propertyTypeDto =
@@ -590,7 +597,7 @@ AND umbracoNode.id <> @id",
// if the Id of the DataType is not set, we resolve it from the db by its PropertyEditorAlias
if (propertyType.DataTypeId == 0 || propertyType.DataTypeId == default)
{
AssignDataTypeFromPropertyEditor(propertyType);
AssignDataTypeIdFromProvidedKeyOrPropertyEditor(propertyType);
}
// validate the alias
@@ -1434,37 +1441,59 @@ AND umbracoNode.id <> @id",
protected abstract TEntity? PerformGet(Guid id);
/// <summary>
/// Try to set the data type id based on its ControlId
/// Try to set the data type Id based on the provided key or property editor alias.
/// </summary>
/// <param name="propertyType"></param>
private void AssignDataTypeFromPropertyEditor(IPropertyType propertyType)
private void AssignDataTypeIdFromProvidedKeyOrPropertyEditor(IPropertyType propertyType)
{
// we cannot try to assign a data type of it's empty
if (propertyType.PropertyEditorAlias.IsNullOrWhiteSpace() == false)
// If a key is provided, use that.
if (propertyType.DataTypeKey != Guid.Empty)
{
Sql<ISqlContext> sql = Sql()
.Select<DataTypeDto>(dt => dt.Select(x => x.NodeDto))
.From<DataTypeDto>()
.InnerJoin<NodeDto>().On<DataTypeDto, NodeDto>((dt, n) => dt.NodeId == n.NodeId)
.Where(
"propertyEditorAlias = @propertyEditorAlias",
new { propertyEditorAlias = propertyType.PropertyEditorAlias })
.OrderBy<DataTypeDto>(typeDto => typeDto.NodeId);
DataTypeDto? datatype = Database.FirstOrDefault<DataTypeDto>(sql);
// we cannot assign a data type if one was not found
if (datatype != null)
Attempt<int> dataTypeIdAttempt = _idKeyMap.Value.GetIdForKey(propertyType.DataTypeKey, UmbracoObjectTypes.DataType);
if (dataTypeIdAttempt.Success)
{
propertyType.DataTypeId = datatype.NodeId;
propertyType.DataTypeKey = datatype.NodeDto.UniqueId;
propertyType.DataTypeId = dataTypeIdAttempt.Result;
return;
}
else
{
Logger.LogWarning(
"Could not assign a data type for the property type {PropertyTypeAlias} since no data type was found with a property editor {PropertyEditorAlias}",
propertyType.Alias, propertyType.PropertyEditorAlias);
"Could not assign a data type for the property type {PropertyTypeAlias} since no integer Id was found matching the key {DataTypeKey}. Falling back to look up via the property editor alias.",
propertyType.Alias,
propertyType.DataTypeKey);
}
}
// Otherwise if a property editor alias is provided, try to find a data type that uses that alias.
if (propertyType.PropertyEditorAlias.IsNullOrWhiteSpace())
{
// We cannot try to assign a data type if it's empty.
return;
}
Sql<ISqlContext> sql = Sql()
.Select<DataTypeDto>(dt => dt.Select(x => x.NodeDto))
.From<DataTypeDto>()
.InnerJoin<NodeDto>().On<DataTypeDto, NodeDto>((dt, n) => dt.NodeId == n.NodeId)
.Where(
"propertyEditorAlias = @propertyEditorAlias",
new { propertyEditorAlias = propertyType.PropertyEditorAlias })
.OrderBy<DataTypeDto>(typeDto => typeDto.NodeId);
DataTypeDto? datatype = Database.FirstOrDefault<DataTypeDto>(sql);
// we cannot assign a data type if one was not found
if (datatype != null)
{
propertyType.DataTypeId = datatype.NodeId;
propertyType.DataTypeKey = datatype.NodeDto.UniqueId;
}
else
{
Logger.LogWarning(
"Could not assign a data type for the property type {PropertyTypeAlias} since no data type was found with a property editor {PropertyEditorAlias}",
propertyType.Alias,
propertyType.PropertyEditorAlias);
}
}
protected abstract TEntity? PerformGet(string alias);
@@ -5,6 +5,7 @@ using Umbraco.Cms.Core.Cache;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Persistence.Querying;
using Umbraco.Cms.Core.Persistence.Repositories;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Strings;
using Umbraco.Cms.Infrastructure.Persistence.Dtos;
using Umbraco.Cms.Infrastructure.Persistence.Querying;
@@ -24,8 +25,9 @@ internal class MediaTypeRepository : ContentTypeRepositoryBase<IMediaType>, IMed
ILogger<MediaTypeRepository> logger,
IContentTypeCommonRepository commonRepository,
ILanguageRepository languageRepository,
IShortStringHelper shortStringHelper)
: base(scopeAccessor, cache, logger, commonRepository, languageRepository, shortStringHelper)
IShortStringHelper shortStringHelper,
Lazy<IIdKeyMap> idKeyMap)
: base(scopeAccessor, cache, logger, commonRepository, languageRepository, shortStringHelper, idKeyMap)
{
}
@@ -5,6 +5,7 @@ using Umbraco.Cms.Core.Cache;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Persistence.Querying;
using Umbraco.Cms.Core.Persistence.Repositories;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Strings;
using Umbraco.Cms.Infrastructure.Persistence.Dtos;
using Umbraco.Cms.Infrastructure.Persistence.Factories;
@@ -27,9 +28,10 @@ internal class MemberTypeRepository : ContentTypeRepositoryBase<IMemberType>, IM
ILogger<MemberTypeRepository> logger,
IContentTypeCommonRepository commonRepository,
ILanguageRepository languageRepository,
IShortStringHelper shortStringHelper)
: base(scopeAccessor, cache, logger, commonRepository, languageRepository, shortStringHelper) =>
_shortStringHelper = shortStringHelper;
IShortStringHelper shortStringHelper,
Lazy<IIdKeyMap> idKeyMap)
: base(scopeAccessor, cache, logger, commonRepository, languageRepository, shortStringHelper, idKeyMap)
=> _shortStringHelper = shortStringHelper;
protected override bool SupportsPublishing => MemberType.SupportsPublishingConst;
@@ -128,10 +128,13 @@ internal class TagRepository : EntityRepositoryBase<int, ITag>, ITagRepository
var group = SqlSyntax.GetQuotedColumnName("group");
// insert tags
// - Note we are checking in the subquery for the existence of the tag, so we don't insert duplicates, using a case-insensitive comparison (the
// LOWER keyword is consistent across SQLite and SQLServer). This ensures consistent behavior across databases as by default, SQLServer will
// perform a case-insensitive comparison, while SQLite will not.
var sql1 = $@"INSERT INTO cmsTags (tag, {group}, languageId)
SELECT tagSet.tag, tagSet.{group}, tagSet.languageId
FROM {tagSetSql}
LEFT OUTER JOIN cmsTags ON (tagSet.tag = cmsTags.tag AND tagSet.{group} = cmsTags.{group} AND COALESCE(tagSet.languageId, -1) = COALESCE(cmsTags.languageId, -1))
LEFT OUTER JOIN cmsTags ON (LOWER(tagSet.tag) = LOWER(cmsTags.tag) AND LOWER(tagSet.{group}) = LOWER(cmsTags.{group}) AND COALESCE(tagSet.languageId, -1) = COALESCE(cmsTags.languageId, -1))
WHERE cmsTags.id IS NULL";
Database.Execute(sql1);
@@ -142,7 +145,7 @@ SELECT {contentId}, {propertyTypeId}, tagSet2.Id
FROM (
SELECT t.Id
FROM {tagSetSql}
INNER JOIN cmsTags as t ON (tagSet.tag = t.tag AND tagSet.{group} = t.{group} AND COALESCE(tagSet.languageId, -1) = COALESCE(t.languageId, -1))
INNER JOIN cmsTags as t ON (LOWER(tagSet.tag) = LOWER(t.tag) AND LOWER(tagSet.{group}) = LOWER(t.{group}) AND COALESCE(tagSet.languageId, -1) = COALESCE(t.languageId, -1))
) AS tagSet2
LEFT OUTER JOIN cmsTagRelationship r ON (tagSet2.id = r.tagId AND r.nodeId = {contentId} AND r.propertyTypeID = {propertyTypeId})
WHERE r.tagId IS NULL";
@@ -245,14 +248,18 @@ WHERE r.tagId IS NULL";
{
public bool Equals(ITag? x, ITag? y) =>
ReferenceEquals(x, y) // takes care of both being null
|| (x != null && y != null && x.Text == y.Text && x.Group == y.Group && x.LanguageId == y.LanguageId);
|| (x != null &&
y != null &&
string.Equals(x.Text, y.Text, StringComparison.OrdinalIgnoreCase) &&
string.Equals(x.Group, y.Group, StringComparison.OrdinalIgnoreCase) &&
x.LanguageId == y.LanguageId);
public int GetHashCode(ITag obj)
{
unchecked
{
var h = obj.Text.GetHashCode();
h = (h * 397) ^ obj.Group.GetHashCode();
var h = StringComparer.OrdinalIgnoreCase.GetHashCode(obj.Text);
h = (h * 397) ^ StringComparer.OrdinalIgnoreCase.GetHashCode(obj.Group);
h = (h * 397) ^ (obj.LanguageId?.GetHashCode() ?? 0);
return h;
}
@@ -372,15 +372,17 @@ SELECT 4 AS [Key], COUNT(id) AS [Value] FROM umbracoUser WHERE userDisabled = 0
List<int> userIds = dtos.Count == 1 ? new List<int> {dtos[0].Id} : dtos.Select(x => x.Id).ToList();
Dictionary<int, UserDto>? xUsers = dtos.Count == 1 ? null : dtos.ToDictionary(x => x.Id, x => x);
Sql<ISqlContext> sql;
// get users2groups
Sql<ISqlContext> sql = SqlContext.Sql()
.Select<User2UserGroupDto>()
.From<User2UserGroupDto>()
.WhereIn<User2UserGroupDto>(x => x.UserId, userIds);
List<User2UserGroupDto>? user2Groups = Database.Fetch<User2UserGroupDto>(sql);
var user2Groups = Database.FetchByGroups<User2UserGroupDto, int>(userIds, Constants.Sql.MaxParameterCount, ints =>
{
return SqlContext.Sql()
.Select<User2UserGroupDto>()
.From<User2UserGroupDto>()
.WhereIn<User2UserGroupDto>(x => x.UserId, ints);
}).ToList();
var groupIds = user2Groups.Select(x => x.UserGroupId).ToList();
// get groups
@@ -422,12 +424,13 @@ SELECT 4 AS [Key], COUNT(id) AS [Value] FROM umbracoUser WHERE userDisabled = 0
// get start nodes
sql = SqlContext.Sql()
.Select<UserStartNodeDto>()
.From<UserStartNodeDto>()
.WhereIn<UserStartNodeDto>(x => x.UserId, userIds);
List<UserStartNodeDto>? startNodes = Database.Fetch<UserStartNodeDto>(sql);
var startNodes = Database.FetchByGroups<UserStartNodeDto, int>(userIds, Constants.Sql.MaxParameterCount, ints =>
{
return SqlContext.Sql()
.Select<UserStartNodeDto>()
.From<UserStartNodeDto>()
.WhereIn<UserStartNodeDto>(x => x.UserId, ints);
}).ToList();
// get groups2languages
@@ -1085,19 +1088,20 @@ SELECT 4 AS [Key], COUNT(id) AS [Value] FROM umbracoUser WHERE userDisabled = 0
/// <inheritdoc />
public void InvalidateSessionsForRemovedProviders(IEnumerable<string> currentLoginProviders)
{
// Get all the user or member keys associated with the removed providers.
// Get all the user keys associated with the removed providers.
Sql<ISqlContext> idsQuery = SqlContext.Sql()
.Select<ExternalLoginDto>(x => x.UserOrMemberKey)
.From<ExternalLoginDto>()
.Where<ExternalLoginDto>(x => !x.LoginProvider.StartsWith(Constants.Security.MemberExternalAuthenticationTypePrefix)) // Only invalidate sessions relating to backoffice users, not members.
.WhereNotIn<ExternalLoginDto>(x => x.LoginProvider, currentLoginProviders);
List<Guid> userAndMemberKeysAssociatedWithRemovedProviders = Database.Fetch<Guid>(idsQuery);
if (userAndMemberKeysAssociatedWithRemovedProviders.Count == 0)
List<Guid> userKeysAssociatedWithRemovedProviders = Database.Fetch<Guid>(idsQuery);
if (userKeysAssociatedWithRemovedProviders.Count == 0)
{
return;
}
// Filter for actual users and convert to integer IDs.
var userIdsAssociatedWithRemovedProviders = userAndMemberKeysAssociatedWithRemovedProviders
// Convert to user integer IDs.
var userIdsAssociatedWithRemovedProviders = userKeysAssociatedWithRemovedProviders
.Select(ConvertUserKeyToUserId)
.Where(x => x.HasValue)
.Select(x => x!.Value)
@@ -1119,7 +1123,6 @@ SELECT 4 AS [Key], COUNT(id) AS [Value] FROM umbracoUser WHERE userDisabled = 0
// User Ids are stored as integers in the umbracoUser table, but as a GUID representation
// of that integer in umbracoExternalLogin (converted via IntExtensions.ToGuid()).
// We need to parse that to get the user Ids to invalidate.
// Note also that umbracoExternalLogin contains members too, as proper GUIDs, so we need to ignore them.
IntExtensions.TryParseFromGuid(userOrMemberKey, out int? userId) ? userId : null;
#endregion
@@ -88,7 +88,7 @@ internal abstract class BlockEditorPropertyValueEditor : BlockValuePropertyValue
{
var rawJson = value == null ? string.Empty : value is string str ? str : value.ToString();
BlockEditorData? blockEditorData = BlockEditorValues.DeserializeAndClean(rawJson);
BlockEditorData? blockEditorData = SafeParseBlockEditorData(rawJson);
if (blockEditorData is null)
{
yield break;
@@ -115,17 +115,7 @@ internal abstract class BlockEditorPropertyValueEditor : BlockValuePropertyValue
{
var val = property.GetValue(culture, segment);
BlockEditorData? blockEditorData;
try
{
blockEditorData = BlockEditorValues.DeserializeAndClean(val);
}
catch (JsonSerializationException)
{
// if this occurs it means the data is invalid, shouldn't happen but has happened if we change the data format.
return string.Empty;
}
BlockEditorData? blockEditorData = SafeParseBlockEditorData(val);
if (blockEditorData == null)
{
return string.Empty;
@@ -150,17 +140,7 @@ internal abstract class BlockEditorPropertyValueEditor : BlockValuePropertyValue
return null;
}
BlockEditorData? blockEditorData;
try
{
blockEditorData = BlockEditorValues.DeserializeAndClean(editorValue.Value);
}
catch (JsonSerializationException)
{
// if this occurs it means the data is invalid, shouldn't happen but has happened if we change the data format.
return string.Empty;
}
BlockEditorData? blockEditorData = SafeParseBlockEditorData(editorValue.Value);
if (blockEditorData == null || blockEditorData.BlockValue.ContentData.Count == 0)
{
return string.Empty;
@@ -171,4 +151,23 @@ internal abstract class BlockEditorPropertyValueEditor : BlockValuePropertyValue
// return json
return JsonConvert.SerializeObject(blockEditorData.BlockValue, Formatting.None);
}
// We don't throw on error here because we want to be able to parse what we can, even if some of the data is invalid. In cases where migrating
// from nested content to blocks, we don't want to trigger a fatal error for retrieving references, as this isn't vital to the operation.
// See: https://github.com/umbraco/Umbraco-CMS/issues/19784 and Umbraco support cases.
private BlockEditorData? SafeParseBlockEditorData(object? value)
{
try
{
return BlockEditorValues.DeserializeAndClean(value);
}
catch (JsonSerializationException ex)
{
_logger.LogWarning(
"Could not deserialize the provided property value into a block editor value: {PropertyValue}. Error: {ErrorMessage}.",
value,
ex.Message);
return null;
}
}
}
@@ -2,6 +2,7 @@
// See LICENSE for more details.
using Microsoft.Extensions.Options;
using Umbraco.Cms.Core.Cache;
using Umbraco.Cms.Core.Configuration.Models;
using Umbraco.Cms.Core.IO;
using Umbraco.Cms.Core.Models.Editors;
@@ -16,11 +17,18 @@ namespace Umbraco.Cms.Core.PropertyEditors;
/// <summary>
/// The value editor for the file upload property editor.
/// </summary>
internal class FileUploadPropertyValueEditor : DataValueEditor
/// <remarks>
/// As this class is loaded into <see cref="ValueEditorCache"/> which can be cleared, it needs
/// to be disposable in order to properly clean up resources such as
/// the settings change subscription and avoid a memory leak.
/// </remarks>
internal class FileUploadPropertyValueEditor : DataValueEditor, IDisposable
{
private readonly MediaFileManager _mediaFileManager;
private readonly IFileStreamSecurityValidator _fileStreamSecurityValidator;
private ContentSettings _contentSettings;
private readonly IDisposable? _contentSettingsChangeSubscription;
public FileUploadPropertyValueEditor(
DataEditorAttribute attribute,
@@ -36,7 +44,7 @@ internal class FileUploadPropertyValueEditor : DataValueEditor
_mediaFileManager = mediaFileManager ?? throw new ArgumentNullException(nameof(mediaFileManager));
_fileStreamSecurityValidator = fileStreamSecurityValidator;
_contentSettings = contentSettings.CurrentValue ?? throw new ArgumentNullException(nameof(contentSettings));
contentSettings.OnChange(x => _contentSettings = x);
_contentSettingsChangeSubscription = contentSettings.OnChange(x => _contentSettings = x);
}
/// <summary>
@@ -163,4 +171,6 @@ internal class FileUploadPropertyValueEditor : DataValueEditor
return filepath;
}
public void Dispose() => _contentSettingsChangeSubscription?.Dispose();
}
@@ -21,6 +21,10 @@ namespace Umbraco.Cms.Core.PropertyEditors;
/// <summary>
/// Represents an image cropper property editor.
/// </summary>
/// <remarks>
/// As this class is not registered with DI as a singleton, it must be disposed to release
/// the settings change subscription and avoid a memory leak.
/// </remarks>
[DataEditor(
Constants.PropertyEditors.Aliases.ImageCropper,
"Image Cropper",
@@ -42,6 +46,7 @@ public class ImageCropperPropertyEditor : DataEditor, IMediaUrlGenerator,
private readonly IIOHelper _ioHelper;
private readonly ILogger<ImageCropperPropertyEditor> _logger;
private readonly MediaFileManager _mediaFileManager;
private ContentSettings _contentSettings;
// Scheduled for removal in v12
@@ -23,13 +23,20 @@ namespace Umbraco.Cms.Core.PropertyEditors;
/// <summary>
/// The value editor for the image cropper property editor.
/// </summary>
internal class ImageCropperPropertyValueEditor : DataValueEditor // TODO: core vs web?
/// <remarks>
/// As this class is loaded into <see cref="ValueEditorCache"/> which can be cleared, it needs
/// to be disposable in order to properly clean up resources such as
/// the settings change subscription and avoid a memory leak.
/// </remarks>
internal class ImageCropperPropertyValueEditor : DataValueEditor, IDisposable
{
private readonly IDataTypeConfigurationCache _dataTypeConfigurationCache;
private readonly IFileStreamSecurityValidator _fileStreamSecurityValidator;
private readonly ILogger<ImageCropperPropertyValueEditor> _logger;
private readonly MediaFileManager _mediaFileManager;
private ContentSettings _contentSettings;
private readonly IDisposable? _contentSettingsChangeSubscription;
public ImageCropperPropertyValueEditor(
DataEditorAttribute attribute,
@@ -49,7 +56,7 @@ internal class ImageCropperPropertyValueEditor : DataValueEditor // TODO: core v
_contentSettings = contentSettings.CurrentValue;
_dataTypeConfigurationCache = dataTypeConfigurationCache;
_fileStreamSecurityValidator = fileStreamSecurityValidator;
contentSettings.OnChange(x => _contentSettings = x);
_contentSettingsChangeSubscription = contentSettings.OnChange(x => _contentSettings = x);
}
/// <summary>
@@ -252,4 +259,6 @@ internal class ImageCropperPropertyValueEditor : DataValueEditor // TODO: core v
return filepath;
}
public void Dispose() => _contentSettingsChangeSubscription?.Dispose();
}
@@ -1,5 +1,6 @@
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using System.Text.RegularExpressions;
using Umbraco.Cms.Core.Configuration.Models;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.Blocks;
@@ -50,9 +51,11 @@ internal class RichTextPropertyIndexValueFactory : NestedPropertyIndexValueFacto
: null;
// index the stripped HTML values combined with "blocks values resume" value
var richTextWithoutMarkup = StripHtmlForIndexing(richTextEditorValue.Markup);
yield return new KeyValuePair<string, IEnumerable<object?>>(
property.Alias,
new object[] { $"{richTextEditorValue.Markup.StripHtml()} {blocksIndexValuesResume}" });
new object[] { $"{richTextWithoutMarkup} {blocksIndexValuesResume}" });
// store the raw value
yield return new KeyValuePair<string, IEnumerable<object?>>(
@@ -75,4 +78,28 @@ internal class RichTextPropertyIndexValueFactory : NestedPropertyIndexValueFacto
protected override IEnumerable<BlockItemData> GetDataItems(RichTextEditorValue input)
=> input.Blocks?.ContentData ?? new List<BlockItemData>();
/// <summary>
/// Strips HTML tags from content while preserving whitespace from line breaks.
/// This addresses the issue where &lt;br&gt; tags don't create word boundaries when HTML is stripped.
/// </summary>
/// <param name="html">The HTML content to strip</param>
/// <returns>Plain text with proper word boundaries</returns>
private static string StripHtmlForIndexing(string html)
{
if (string.IsNullOrWhiteSpace(html))
{
return string.Empty;
}
// Replace <br> and <br/> tags (with any amount of whitespace and attributes) with spaces
// This regex matches:
// - <br> (with / without spaces or attributes)
// - <br /> (with / without spaces or attributes)
html = Regex.Replace(html, @"<br\b[^>]*/?>\s*", " ", RegexOptions.IgnoreCase);
// Use the existing Microsoft StripHtml function for everything else
return html.StripHtml();
}
}
@@ -11,10 +11,19 @@ using Umbraco.Extensions;
namespace Umbraco.Cms.Core.PropertyEditors;
internal class UploadFileTypeValidator : IValueValidator
/// <summary>
/// The value editor for file upload property editors.
/// </summary>
/// <remarks>
/// As this class is not registered with DI as a singleton, it must be disposed to release
/// the settings change subscription and avoid a memory leak.
/// </remarks>
internal class UploadFileTypeValidator : IValueValidator, IDisposable
{
private readonly ILocalizedTextService _localizedTextService;
private ContentSettings _contentSettings;
private readonly IDisposable? _contentSettingsChangeSubscription;
public UploadFileTypeValidator(
ILocalizedTextService localizedTextService,
@@ -23,7 +32,7 @@ internal class UploadFileTypeValidator : IValueValidator
_localizedTextService = localizedTextService;
_contentSettings = contentSettings.CurrentValue;
contentSettings.OnChange(x => _contentSettings = x);
_contentSettingsChangeSubscription = contentSettings.OnChange(x => _contentSettings = x);
}
public IEnumerable<ValidationResult> Validate(object? value, string? valueType, object? dataTypeConfiguration)
@@ -103,4 +112,6 @@ internal class UploadFileTypeValidator : IValueValidator
extension = fileName.GetFileExtension().TrimStart(".");
return true;
}
public void Dispose() => _contentSettingsChangeSubscription?.Dispose();
}
@@ -31,8 +31,12 @@ namespace Umbraco.Cms.Core.PropertyEditors.ValueConverters;
/// A value converter for TinyMCE that will ensure any macro content is rendered properly even when
/// used dynamically.
/// </summary>
/// <remarks>
/// As this class is not registered with DI as a singleton, it must be disposed to release
/// the settings change subscription and avoid a memory leak.
/// </remarks>
[DefaultPropertyValueConverter]
public class RteMacroRenderingValueConverter : SimpleTinyMceValueConverter, IDeliveryApiPropertyValueConverter
public class RteMacroRenderingValueConverter : SimpleTinyMceValueConverter, IDeliveryApiPropertyValueConverter, IDisposable
{
private readonly HtmlImageSourceParser _imageSourceParser;
private readonly HtmlLocalLinkParser _linkParser;
@@ -47,7 +51,9 @@ public class RteMacroRenderingValueConverter : SimpleTinyMceValueConverter, IDel
private readonly ILogger<RteMacroRenderingValueConverter> _logger;
private readonly IApiElementBuilder _apiElementBuilder;
private readonly RichTextBlockPropertyValueConstructorCache _constructorCache;
private DeliveryApiSettings _deliveryApiSettings;
private readonly IDisposable? _deliveryApiSettingsChangeSubscription;
[Obsolete("Please use the constructor that takes all arguments. Will be removed in V14.")]
public RteMacroRenderingValueConverter(IUmbracoContextAccessor umbracoContextAccessor, IMacroRenderer macroRenderer,
@@ -107,8 +113,9 @@ public class RteMacroRenderingValueConverter : SimpleTinyMceValueConverter, IDel
_apiElementBuilder = apiElementBuilder;
_constructorCache = constructorCache;
_logger = logger;
_deliveryApiSettings = deliveryApiSettingsMonitor.CurrentValue;
deliveryApiSettingsMonitor.OnChange(settings => _deliveryApiSettings = settings);
_deliveryApiSettingsChangeSubscription = deliveryApiSettingsMonitor.OnChange(settings => _deliveryApiSettings = settings);
}
public override PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType) =>
@@ -314,4 +321,6 @@ public class RteMacroRenderingValueConverter : SimpleTinyMceValueConverter, IDel
public required RichTextBlockModel? RichTextBlockModel { get; set; }
}
public void Dispose() => _deliveryApiSettingsChangeSubscription?.Dispose();
}
@@ -153,6 +153,9 @@ public class PublishedContentQuery : IPublishedContentQuery
public IEnumerable<IPublishedContent> ContentAtRoot()
=> ItemsAtRoot(_publishedSnapshot.Content);
public IEnumerable<IPublishedContent> ContentAtRoot(string? culture)
=> ItemsAtRoot(_publishedSnapshot.Content, culture);
#endregion
#region Media
@@ -232,8 +235,8 @@ public class PublishedContentQuery : IPublishedContentQuery
IPublishedCache? cache)
=> cache?.GetByXPath(xpath, vars) ?? Array.Empty<IPublishedContent>();
private static IEnumerable<IPublishedContent> ItemsAtRoot(IPublishedCache? cache)
=> cache?.GetAtRoot() ?? Array.Empty<IPublishedContent>();
private static IEnumerable<IPublishedContent> ItemsAtRoot(IPublishedCache? cache, string? culture = null)
=> cache?.GetAtRoot(culture) ?? Array.Empty<IPublishedContent>();
#endregion
@@ -95,6 +95,7 @@ namespace Umbraco.Cms.Infrastructure.Routing
using UmbracoContextReference reference = _umbracoContextFactory.EnsureUmbracoContext();
IPublishedContentCache? contentCache = reference.UmbracoContext.Content;
if (contentCache == null)
{
_logger.LogWarning("Could not track redirects because there is no published content cache available on the current published snapshot.");
@@ -106,11 +107,16 @@ namespace Umbraco.Cms.Infrastructure.Routing
try
{
var newRoute = contentCache.GetRouteById(contentId, culture);
if (!IsValidRoute(newRoute) || oldRoute == newRoute)
{
continue;
}
// Ensure we don't create a self-referencing redirect. This can occur if a document is renamed and then the name is reverted back
// to the original. We resolve this by removing any existing redirect that points to the new route.
RemoveSelfReferencingRedirect(contentKey, newRoute);
_redirectUrlService.Register(oldRoute, contentKey, culture);
}
catch (Exception ex)
@@ -121,5 +127,17 @@ namespace Umbraco.Cms.Infrastructure.Routing
}
private static bool IsValidRoute([NotNullWhen(true)] string? route) => route is not null && !route.StartsWith("err/");
private void RemoveSelfReferencingRedirect(Guid contentKey, string route)
{
IEnumerable<IRedirectUrl> allRedirectUrls = _redirectUrlService.GetContentRedirectUrls(contentKey);
foreach (IRedirectUrl redirectUrl in allRedirectUrls)
{
if (redirectUrl.Url == route)
{
_redirectUrlService.Delete(redirectUrl.Key);
}
}
}
}
}
@@ -52,16 +52,16 @@ public sealed class ContentTypeIndexingNotificationHandler : INotificationHandle
throw new NotSupportedException();
}
var changedIds = new Dictionary<string, (List<int> removedIds, List<int> refreshedIds, List<int> otherIds)>();
var changedIds = new Dictionary<string, (List<int> removedIds, List<int> refreshedIds)>();
foreach (ContentTypeCacheRefresher.JsonPayload payload in (ContentTypeCacheRefresher.JsonPayload[])args
.MessageObject)
{
if (!changedIds.TryGetValue(
payload.ItemType,
out (List<int> removedIds, List<int> refreshedIds, List<int> otherIds) idLists))
out (List<int> removedIds, List<int> refreshedIds) idLists))
{
idLists = (removedIds: new List<int>(), refreshedIds: new List<int>(), otherIds: new List<int>());
idLists = (removedIds: new List<int>(), refreshedIds: new List<int>());
changedIds.Add(payload.ItemType, idLists);
}
@@ -73,28 +73,24 @@ public sealed class ContentTypeIndexingNotificationHandler : INotificationHandle
{
idLists.refreshedIds.Add(payload.Id);
}
else if (payload.ChangeTypes.HasType(ContentTypeChangeTypes.RefreshOther))
{
idLists.otherIds.Add(payload.Id);
}
}
foreach (KeyValuePair<string, (List<int> removedIds, List<int> refreshedIds, List<int> otherIds)> ci in
foreach (KeyValuePair<string, (List<int> removedIds, List<int> refreshedIds)> ci in
changedIds)
{
if (ci.Value.refreshedIds.Count > 0 || ci.Value.otherIds.Count > 0)
if (ci.Value.refreshedIds.Count > 0)
{
switch (ci.Key)
{
case var itemType when itemType == typeof(IContentType).Name:
RefreshContentOfContentTypes(ci.Value.refreshedIds.Concat(ci.Value.otherIds).Distinct()
RefreshContentOfContentTypes(ci.Value.refreshedIds.Distinct()
.ToArray());
break;
case var itemType when itemType == typeof(IMediaType).Name:
RefreshMediaOfMediaTypes(ci.Value.refreshedIds.Concat(ci.Value.otherIds).Distinct().ToArray());
RefreshMediaOfMediaTypes(ci.Value.refreshedIds.Distinct().ToArray());
break;
case var itemType when itemType == typeof(IMemberType).Name:
RefreshMemberOfMemberTypes(ci.Value.refreshedIds.Concat(ci.Value.otherIds).Distinct()
RefreshMemberOfMemberTypes(ci.Value.refreshedIds.Distinct()
.ToArray());
break;
}
@@ -154,6 +150,10 @@ public sealed class ContentTypeIndexingNotificationHandler : INotificationHandle
const int pageSize = 500;
var page = 0;
var total = long.MaxValue;
// track which Ids have their paths are published
var publishChecked = new Dictionary<int, bool>();
while (page * pageSize < total)
{
IEnumerable<IContent> contentToRefresh = _contentService.GetPagedOfTypes(
@@ -165,20 +165,20 @@ public sealed class ContentTypeIndexingNotificationHandler : INotificationHandle
// order by shallowest to deepest, this allows us to check it's published state without checking every item
Ordering.By("Path"));
// track which Ids have their paths are published
var publishChecked = new Dictionary<int, bool>();
foreach (IContent c in contentToRefresh)
{
var isPublished = false;
if (c.Published)
{
if (!publishChecked.TryGetValue(c.ParentId, out isPublished))
if (publishChecked.TryGetValue(c.ParentId, out isPublished) is false)
{
// nothing by parent id, so query the service and cache the result for the next child to check against
isPublished = _contentService.IsPathPublished(c);
publishChecked[c.Id] = isPublished;
// the parent *must* be published if the entire path is published
publishChecked[c.ParentId] = isPublished;
}
publishChecked[c.Id] = isPublished;
}
_umbracoIndexingHandler.ReIndexForContent(c, isPublished);
@@ -1,4 +1,4 @@
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Umbraco.Cms.Core.Cache;
using Umbraco.Cms.Core.Configuration.Models;
@@ -17,7 +17,7 @@ internal sealed class DeliveryApiContentIndexingNotificationHandler :
{
private readonly IDeliveryApiIndexingHandler _deliveryApiIndexingHandler;
private readonly ILogger<DeliveryApiContentIndexingNotificationHandler> _logger;
private DeliveryApiSettings _deliveryApiSettings;
private readonly DeliveryApiSettings _deliveryApiSettings;
public DeliveryApiContentIndexingNotificationHandler(
IDeliveryApiIndexingHandler deliveryApiIndexingHandler,
@@ -27,7 +27,6 @@ internal sealed class DeliveryApiContentIndexingNotificationHandler :
_deliveryApiIndexingHandler = deliveryApiIndexingHandler;
_logger = logger;
_deliveryApiSettings = deliveryApiSettings.CurrentValue;
deliveryApiSettings.OnChange(settings => _deliveryApiSettings = settings);
}
public void Handle(ContentCacheRefresherNotification notification)
@@ -129,7 +129,7 @@ public class IdentityMapDefinition : IMapDefinition
target.IsApproved = source.IsApproved;
target.SecurityStamp = source.SecurityStamp;
DateTime? lockedOutUntil = source.LastLockoutDate?.AddMinutes(_securitySettings.UserDefaultLockoutTimeInMinutes);
target.LockoutEnd = source.IsLockedOut ? (lockedOutUntil ?? DateTime.MaxValue).ToUniversalTime() : null;
target.LockoutEnd = source.IsLockedOut ? lockedOutUntil ?? DateTime.MaxValue : null;
}
// Umbraco.Code.MapAll -Id -LockoutEnabled -PhoneNumber -PhoneNumberConfirmed -ConcurrencyStamp -NormalizedEmail -NormalizedUserName -Roles
@@ -146,17 +146,46 @@ public class IdentityMapDefinition : IMapDefinition
target.PasswordConfig = source.PasswordConfiguration;
target.IsApproved = source.IsApproved;
target.SecurityStamp = source.SecurityStamp;
DateTime? lockedOutUntil = source.LastLockoutDate?.AddMinutes(_securitySettings.MemberDefaultLockoutTimeInMinutes);
target.LockoutEnd = source.IsLockedOut ? (lockedOutUntil ?? DateTime.MaxValue).ToUniversalTime() : null;
target.LockoutEnd = GetLockoutEnd(source);
target.LastLockoutDateUtc = GetLastLockoutDateUtc(source);
target.Comments = source.Comments;
target.LastLockoutDateUtc = source.LastLockoutDate == DateTime.MinValue
? null
: source.LastLockoutDate?.ToUniversalTime();
target.CreatedDateUtc = source.CreateDate.ToUniversalTime();
target.CreatedDateUtc = EnsureUtcWithServerTime(source.CreateDate);
target.Key = source.Key;
target.MemberTypeAlias = source.ContentTypeAlias;
target.TwoFactorEnabled = _twoFactorLoginService.IsTwoFactorEnabledAsync(source.Key).GetAwaiter().GetResult();
// NB: same comments re AutoMapper as per BackOfficeUser
}
private DateTimeOffset? GetLockoutEnd(IMember source)
{
if (source.IsLockedOut is false)
{
return null;
}
DateTime? lockedOutUntil = source.LastLockoutDate?.AddMinutes(_securitySettings.MemberDefaultLockoutTimeInMinutes);
if (lockedOutUntil.HasValue is false)
{
return DateTimeOffset.MaxValue;
}
return EnsureUtcWithServerTime(lockedOutUntil.Value);
}
private static DateTime? GetLastLockoutDateUtc(IMember source)
{
if (source.LastLockoutDate is null || source.LastLockoutDate == DateTime.MinValue)
{
return null;
}
return EnsureUtcWithServerTime(source.LastLockoutDate.Value);
}
private static DateTime EnsureUtcWithServerTime(DateTime date) =>
// We have a server time value here, but the the Kind is UTC, so we can't use .ToUniversalTime() to convert to the UTC
// value that the LockoutEnd property expects. We need to create a DateTimeOffset with the correct offset.
DateTime.SpecifyKind(date, DateTimeKind.Local).ToUniversalTime();
}
@@ -43,6 +43,10 @@
<!-- Both Dazinator.Extensions.FileProviders and MiniProfiler.AspNetCore.Mvc bring in a vulnerable version of System.Text.RegularExpressions -->
<PackageReference Include="System.Text.RegularExpressions" />
<!-- Take top-level dependency on these Microsoft.CodeAnalysis.* packages due to conflicts when installing Microsoft.EntityFrameworkCore.Design -->
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" />
<PackageReference Include="Microsoft.CodeAnalysis.Workspaces.MSBuild" />
</ItemGroup>
<ItemGroup>
@@ -321,17 +321,12 @@ public class ContentStore
{
if (_writeLock.CurrentCount != 0)
{
throw new InvalidOperationException("Write lock must be acquried.");
throw new InvalidOperationException("Write lock must be acquired.");
}
}
private void Lock(WriteLockInfo lockInfo, bool forceGen = false)
{
if (_writeLock.CurrentCount == 0)
{
throw new InvalidOperationException("Recursive locks not allowed");
}
if (_writeLock.Wait(_monitorTimeout))
{
lockInfo.Taken = true;
@@ -114,7 +114,7 @@ internal class Property : PublishedPropertyBase
// determines whether a property has value
public override bool HasValue(string? culture = null, string? segment = null)
{
_content.VariationContextAccessor.ContextualizeVariation(_variations, _content.Id, ref culture, ref segment);
_content.VariationContextAccessor.ContextualizeVariation(_variations, _content.Id, PropertyType.Alias, ref culture, ref segment);
var value = GetSourceValue(culture, segment);
var hasValue = PropertyType.IsValue(value, PropertyValueLevel.Source);
@@ -148,7 +148,7 @@ internal class Property : PublishedPropertyBase
public override object? GetSourceValue(string? culture = null, string? segment = null)
{
_content.VariationContextAccessor.ContextualizeVariation(_sourceVariations, _content.Id, ref culture, ref segment);
_content.VariationContextAccessor.ContextualizeVariation(_sourceVariations, _content.Id, PropertyType.Alias, ref culture, ref segment);
// source values are tightly bound to the property/schema culture and segment configurations, so we need to
// sanitize the contextualized culture/segment states before using them to access the source values.
@@ -240,7 +240,7 @@ internal class Property : PublishedPropertyBase
EnsureSourceValuesInitialized();
var k = new CompositeStringStringKey(culture, segment);
var k = new CompositeStringStringKey(culture ?? string.Empty, segment ?? string.Empty); // Null values are not valid when creating a CompositeStringStringKey.
SourceInterValue vvalue = _sourceValues!.GetOrAdd(k, _ =>
new SourceInterValue
@@ -262,7 +262,7 @@ internal class Property : PublishedPropertyBase
public override object? GetValue(string? culture = null, string? segment = null)
{
_content.VariationContextAccessor.ContextualizeVariation(_variations, _content.Id, ref culture, ref segment);
_content.VariationContextAccessor.ContextualizeVariation(_variations, _content.Id, PropertyType.Alias, ref culture, ref segment);
object? value;
CacheValue cacheValues = GetCacheValues(PropertyType.CacheLevel).For(culture, segment);
@@ -285,7 +285,7 @@ internal class Property : PublishedPropertyBase
[Obsolete("The current implementation of XPath is suboptimal and will be removed entirely in a future version. Scheduled for removal in v14")]
public override object? GetXPathValue(string? culture = null, string? segment = null)
{
_content.VariationContextAccessor.ContextualizeVariation(_variations, _content.Id, ref culture, ref segment);
_content.VariationContextAccessor.ContextualizeVariation(_variations, _content.Id, PropertyType.Alias, ref culture, ref segment);
CacheValue cacheValues = GetCacheValues(PropertyType.CacheLevel).For(culture, segment);
@@ -304,7 +304,7 @@ internal class Property : PublishedPropertyBase
public override object? GetDeliveryApiValue(bool expanding, string? culture = null, string? segment = null)
{
_content.VariationContextAccessor.ContextualizeVariation(_variations, _content.Id, ref culture, ref segment);
_content.VariationContextAccessor.ContextualizeVariation(_variations, _content.Id, PropertyType.Alias, ref culture, ref segment);
object? value;
CacheValue cacheValues = GetCacheValues(expanding ? PropertyType.DeliveryApiCacheLevelForExpansion : PropertyType.DeliveryApiCacheLevel).For(culture, segment);
@@ -53,9 +53,7 @@ public class PublishedSnapshotServiceEventHandler :
public void Handle(ContentTypeRefreshedNotification notification)
{
const ContentTypeChangeTypes types // only for those that have been refreshed
= ContentTypeChangeTypes.RefreshMain | ContentTypeChangeTypes.RefreshOther;
var contentTypeIds = notification.Changes.Where(x => x.ChangeTypes.HasTypesAny(types)).Select(x => x.Item.Id)
var contentTypeIds = notification.Changes.Where(x => x.ChangeTypes.IsStructuralChange()).Select(x => x.Item.Id)
.ToArray();
if (contentTypeIds.Any())
{
@@ -44,6 +44,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers;
[PluginController(Constants.Web.Mvc.BackOfficeApiArea)]
[Authorize(Policy = AuthorizationPolicies.TreeAccessDocuments)]
[ParameterSwapControllerActionSelector(nameof(GetById), "id", typeof(int), typeof(Guid), typeof(Udi))]
[ParameterSwapControllerActionSelector(nameof(GetByIds), "ids", typeof(int[]), typeof(Guid[]), typeof(Udi[]))]
[ParameterSwapControllerActionSelector(nameof(GetNiceUrl), "id", typeof(int), typeof(Guid), typeof(Udi))]
public class ContentController : ContentControllerBase
{
@@ -247,6 +248,30 @@ public class ContentController : ContentControllerBase
return foundContent.Select(MapToDisplay).WhereNotNull();
}
/// <summary>
/// Return content for the specified ids
/// </summary>
/// <param name="ids"></param>
/// <returns></returns>
[FilterAllowedOutgoingContent(typeof(IEnumerable<ContentItemDisplay>))]
public IEnumerable<ContentItemDisplay> GetByIds([FromQuery] Guid[] ids)
{
IEnumerable<IContent> foundContent = _contentService.GetByIds(ids);
return foundContent.Select(MapToDisplay).WhereNotNull();
}
/// <summary>
/// Return content for the specified ids
/// </summary>
/// <param name="ids"></param>
/// <returns></returns>
[FilterAllowedOutgoingContent(typeof(IEnumerable<ContentItemDisplay>))]
public IEnumerable<ContentItemDisplay> GetByIds([FromQuery] Udi[] ids)
{
IEnumerable<IContent> foundContent = _contentService.GetByIds(ids)!;
return foundContent.Select(MapToDisplay).WhereNotNull();
}
/// <summary>
/// Updates the permissions for a content item for a particular user group
/// </summary>
@@ -674,8 +674,21 @@ public class ContentTypeController : ContentTypeControllerBase<IContentType>
[Authorize(Policy = AuthorizationPolicies.TreeAccessDocumentTypes)]
public IActionResult Import(string file)
{
if (string.IsNullOrWhiteSpace(file))
{
return NotFound();
}
// The incoming 'file' parameter we expect to contain the just a file name and extension.
// We accept only this and no input parameters containing paths, to prevent any path based security exploits.
var invalidFileNameChars = Path.GetInvalidFileNameChars();
if (file.IndexOfAny(invalidFileNameChars) >= 0 || file.Contains(Path.DirectorySeparatorChar) || file.Contains(Path.AltDirectorySeparatorChar))
{
return NotFound();
}
var filePath = Path.Combine(_hostingEnvironment.MapPathContentRoot(Constants.SystemDirectories.TempFileUploads), file);
if (string.IsNullOrEmpty(file) || !System.IO.File.Exists(filePath))
if (System.IO.File.Exists(filePath) is false)
{
return NotFound();
}
@@ -1,27 +1,27 @@
using System.Xml;
using System.Globalization;
using System.Net.Mime;
using System.Text;
using System.Xml;
using System.Xml.Linq;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Configuration.Models;
using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Core.Hosting;
using Umbraco.Cms.Core.Mapping;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.ContentEditing;
using Umbraco.Cms.Core.Hosting;
using Umbraco.Cms.Core.Security;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Infrastructure.Packaging;
using Umbraco.Cms.Web.Common.Attributes;
using Umbraco.Cms.Web.Common.Authorization;
using Umbraco.Extensions;
using Umbraco.Cms.Infrastructure.Packaging;
using System.Xml.Linq;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
namespace Umbraco.Cms.Web.BackOffice.Controllers;
@@ -460,7 +460,17 @@ public class DictionaryController : BackOfficeNotificationsController
return NotFound();
}
var filePath = Path.Combine(_hostingEnvironment.MapPathContentRoot(Constants.SystemDirectories.Data), file);
// The incoming 'file' parameter we expect to contain the full path to the uploaded file.
// We accept only files coming from the uploads folder to prevent any path based security exploits.
var fileName = Path.GetFileName(file);
var invalidFileNameChars = Path.GetInvalidFileNameChars();
if (fileName.IndexOfAny(invalidFileNameChars) >= 0 || fileName.Contains(Path.DirectorySeparatorChar) || fileName.Contains(Path.AltDirectorySeparatorChar))
{
return NotFound();
}
var root = _hostingEnvironment.MapPathContentRoot(Constants.SystemDirectories.TempFileUploads);
var filePath = Path.Combine(root, fileName);
if (!System.IO.File.Exists(filePath))
{
return NotFound();
@@ -847,6 +847,21 @@ public class EntityController : UmbracoAuthorizedJsonController
return NotFound();
}
EntityBasic? MapEntityBasic(IEntitySlim source, string? culture)
{
EntityBasic? target = _umbracoMapper.Map<IEntitySlim, EntityBasic>(source, context =>
{
context.SetCulture(culture);
});
if (target is not null)
{
target.AdditionalData["hasChildren"] = source.HasChildren;
}
return target;
}
UmbracoObjectTypes? objectType = ConvertToObjectType(type);
if (objectType.HasValue)
{
@@ -857,12 +872,25 @@ public class EntityController : UmbracoAuthorizedJsonController
var ignoreUserStartNodes = IsDataTypeIgnoringUserStartNodes(dataTypeKey);
var culture = ClientCulture();
// root is special: we reduce it to start nodes if the user's start node is not the default, then we need to return their start nodes
if (id == Constants.System.Root && startNodeIds.Length > 0 &&
startNodeIds.Contains(Constants.System.Root) == false && !ignoreUserStartNodes)
startNodeIds.Contains(Constants.System.Root) == false &&
ignoreUserStartNodes == false)
{
return new PagedResult<EntityBasic>(0, 0, 0);
var startNodeEntities = _entityService.GetAll(objectType.Value, startNodeIds).ToList();
IEnumerable<IEntitySlim> pagedStartNodeEntities = startNodeEntities
.Skip((pageNumber - 1) * pageSize)
.Take(pageSize);
return new PagedResult<EntityBasic>(startNodeEntities.Count, pageNumber, pageSize)
{
Items = pagedStartNodeEntities
.Select(source => MapEntityBasic(source, culture))
.WhereNotNull(),
};
}
//adding multiple conditions ,considering id,key & name as filter param
//for id as int
int.TryParse(filter, out int filterAsIntId);
@@ -882,13 +910,11 @@ public class EntityController : UmbracoAuthorizedJsonController
|| x.Key == filterAsGuid),
Ordering.By(orderBy, orderDirection));
if (totalRecords == 0)
{
return new PagedResult<EntityBasic>(0, 0, 0);
}
var culture = ClientCulture();
var pagedResult = new PagedResult<EntityBasic>(totalRecords, pageNumber, pageSize)
{
Items = entities
@@ -898,22 +924,8 @@ public class EntityController : UmbracoAuthorizedJsonController
(objectType == UmbracoObjectTypes.Document || objectType == UmbracoObjectTypes.Media) is false ||
(ContentPermissions.IsInBranchOfStartNode(entity.Path, startNodeIds, startNodePaths, out var hasPathAccess) &&
hasPathAccess))
.Select(source =>
{
EntityBasic? target = _umbracoMapper.Map<IEntitySlim, EntityBasic>(source, context =>
{
context.SetCulture(culture);
context.SetCulture(culture);
});
if (target is not null)
{
//TODO: Why is this here and not in the mapping?
target.AdditionalData["hasChildren"] = source.HasChildren;
}
return target;
}).WhereNotNull()
.Select(source => MapEntityBasic(source, culture))
.WhereNotNull(),
};
return pagedResult;
@@ -1126,7 +1138,7 @@ public class EntityController : UmbracoAuthorizedJsonController
.Select(s => int.Parse(s, CultureInfo.InvariantCulture)).Distinct().ToArray();
var ignoreUserStartNodes =
IsDataTypeIgnoringUserStartNodes(queryStrings?.GetValue<Guid?>("dataTypeId"));
IsDataTypeIgnoringUserStartNodes(queryStrings?.GetValue<Guid?>("dataTypeKey"));
if (ignoreUserStartNodes == false)
{
int[]? aids = null;
@@ -6,7 +6,6 @@ using Microsoft.Extensions.Options;
using Newtonsoft.Json;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Configuration.Models;
using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Web.Common.Attributes;
namespace Umbraco.Cms.Web.BackOffice.Controllers;
@@ -16,7 +15,8 @@ public class HelpController : UmbracoAuthorizedJsonController
{
private static HttpClient? _httpClient;
private readonly ILogger<HelpController> _logger;
private HelpPageSettings? _helpPageSettings;
private readonly HelpPageSettings? _helpPageSettings;
[ActivatorUtilitiesConstructor]
public HelpController(
@@ -25,12 +25,9 @@ public class HelpController : UmbracoAuthorizedJsonController
{
_logger = logger;
ResetHelpPageSettings(helpPageSettings.CurrentValue);
helpPageSettings.OnChange(ResetHelpPageSettings);
_helpPageSettings = helpPageSettings.CurrentValue;
}
private void ResetHelpPageSettings(HelpPageSettings settings) => _helpPageSettings = settings;
public async Task<List<HelpPage>> GetContextHelpForPage(string section, string tree,
string baseUrl = "https://our.umbraco.com")
{
@@ -23,7 +23,7 @@ public class ImagesController : UmbracoAuthorizedApiController
{
private readonly MediaFileManager _mediaFileManager;
private readonly IImageUrlGenerator _imageUrlGenerator;
private ContentSettings _contentSettings;
private readonly ContentSettings _contentSettings;
[Obsolete("Use non obsolete-constructor. Scheduled for removal in Umbraco 13.")]
public ImagesController(
@@ -45,8 +45,6 @@ public class ImagesController : UmbracoAuthorizedApiController
_mediaFileManager = mediaFileManager;
_imageUrlGenerator = imageUrlGenerator;
_contentSettings = contentSettingsMonitor.CurrentValue;
contentSettingsMonitor.OnChange(x => _contentSettings = x);
}
/// <summary>
@@ -47,6 +47,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers;
[PluginController(Constants.Web.Mvc.BackOfficeApiArea)]
[Authorize(Policy = AuthorizationPolicies.SectionAccessMedia)]
[ParameterSwapControllerActionSelector(nameof(GetById), "id", typeof(int), typeof(Guid), typeof(Udi))]
[ParameterSwapControllerActionSelector(nameof(GetByIds), "ids", typeof(int[]), typeof(Guid[]), typeof(Udi[]))]
[ParameterSwapControllerActionSelector(nameof(GetChildren), "id", typeof(int), typeof(Guid), typeof(Udi))]
public class MediaController : ContentControllerBase
{
@@ -296,6 +297,30 @@ public class MediaController : ContentControllerBase
return foundMedia.Select(media => _umbracoMapper.Map<MediaItemDisplay>(media));
}
/// <summary>
/// Return media for the specified ids
/// </summary>
/// <param name="ids"></param>
/// <returns></returns>
[FilterAllowedOutgoingMedia(typeof(IEnumerable<MediaItemDisplay>))]
public IEnumerable<MediaItemDisplay?> GetByIds([FromQuery] Guid[] ids)
{
IEnumerable<IMedia> foundMedia = _mediaService.GetByIds(ids);
return foundMedia.Select(media => _umbracoMapper.Map<MediaItemDisplay>(media));
}
/// <summary>
/// Return media for the specified ids
/// </summary>
/// <param name="ids"></param>
/// <returns></returns>
[FilterAllowedOutgoingMedia(typeof(IEnumerable<MediaItemDisplay>))]
public IEnumerable<MediaItemDisplay?> GetByIds([FromQuery] Udi[] ids)
{
IEnumerable<IMedia> foundMedia = _mediaService.GetByIds(ids);
return foundMedia.Select(media => _umbracoMapper.Map<MediaItemDisplay>(media));
}
/// <summary>
/// Returns a paged result of media items known to be of a "Folder" type
/// </summary>
@@ -178,26 +178,31 @@ public partial class PreviewController : Controller
return RedirectPermanent($"../../{id}{query}");
}
private static bool ValidateProvidedCulture(string culture)
/// <summary>
/// Validates the provided culture code.
/// </summary>
/// <remarks>
/// Marked as internal to expose for unit tests.
/// </remarks>
internal static bool ValidateProvidedCulture(string culture)
{
if (string.IsNullOrEmpty(culture))
{
return true;
}
// We can be confident the backoffice will have provided a valid culture in linking to the
// preview, so we don't need to check that the culture matches an Umbraco language.
// We are only concerned here with protecting against XSS attacks from a fiddled preview
// URL, so we can just confirm we have a valid culture.
try
{
CultureInfo.GetCultureInfo(culture, true);
return true;
}
catch (CultureNotFoundException)
// Culture codes are expected to match this pattern.
if (CultureCodeRegex().IsMatch(culture) is false)
{
return false;
}
// We can be confident the backoffice will have provided a valid culture in linking to the
// preview, so we don't need to check that the culture matches an Umbraco language (or is even a
// valid culture code).
// We are only concerned here with protecting against XSS attacks from a fiddled preview
// URL, so we can proceed if the the regex is matched.
return true;
}
public ActionResult? EnterPreview(int id)
@@ -261,4 +266,7 @@ public partial class PreviewController : Controller
[GeneratedRegex("^\\/(?<id>\\d*)(\\?culture=(?<culture>[\\w-]*))?$")]
private static partial Regex DefaultPreviewRedirectRegex();
[GeneratedRegex(@"^[a-z]{2,3}[-0-9a-z]*$", RegexOptions.IgnoreCase)]
private static partial Regex CultureCodeRegex();
}
@@ -1,6 +1,7 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Logging;
using Umbraco.Cms.Core.DeliveryApi;
using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Core.Hosting;
using Umbraco.Cms.Core.IO;
@@ -21,6 +22,8 @@ using Umbraco.Cms.Web.BackOffice.Security;
using Umbraco.Cms.Web.BackOffice.Services;
using Umbraco.Cms.Web.BackOffice.SignalR;
using Umbraco.Cms.Web.BackOffice.Trees;
using Umbraco.Cms.Web.Common.Accessors;
using Umbraco.Cms.Web.Common.Rendering;
namespace Umbraco.Extensions;
@@ -121,6 +124,9 @@ public static partial class UmbracoBuilderExtensions
builder.Services.AddSingleton<UnhandledExceptionLoggerMiddleware>();
builder.Services.AddTransient<BlockGridSampleHelper>();
builder.Services.AddUnique<IWebhookPresentationFactory, WebhookPresentationFactory>();
// deliveryApi will overwrite these more basic ones.
builder.Services.AddScoped<IOutputExpansionStrategy, ElementOnlyOutputExpansionStrategy>();
builder.Services.AddSingleton<IOutputExpansionStrategyAccessor, RequestContextOutputExpansionStrategyAccessor>();
return builder;
}
@@ -100,7 +100,13 @@ public class ContentTreeController : ContentTreeControllerBase, ISearchableTreeW
public async Task<EntitySearchResults> SearchAsync(string query, int pageSize, long pageIndex, string? searchFrom = null)
{
IEnumerable<SearchResultEntity> results = _treeSearcher.ExamineSearch(query, UmbracoEntityTypes.Document, pageSize, pageIndex, out var totalFound, searchFrom);
IEnumerable<SearchResultEntity> results = _treeSearcher.ExamineSearch(
query,
UmbracoEntityTypes.Document,
pageSize,
pageIndex,
out var totalFound,
searchFrom: searchFrom);
return new EntitySearchResults(results, totalFound);
}
@@ -399,7 +405,14 @@ public class ContentTreeController : ContentTreeControllerBase, ISearchableTreeW
public async Task<EntitySearchResults> SearchAsync(string query, int pageSize, long pageIndex, string? searchFrom = null, string? culture = null)
{
var results = _treeSearcher.ExamineSearch(query, UmbracoEntityTypes.Document, pageSize, pageIndex, out long totalFound, culture: culture, searchFrom: searchFrom);
var results = _treeSearcher.ExamineSearch(
query,
UmbracoEntityTypes.Document,
pageSize,
pageIndex,
out long totalFound,
culture: culture,
searchFrom: searchFrom);
return new EntitySearchResults(results, totalFound);
}
}
@@ -76,8 +76,13 @@ public class MediaTreeController : ContentTreeControllerBase, ISearchableTree, I
public async Task<EntitySearchResults> SearchAsync(string query, int pageSize, long pageIndex,
string? searchFrom = null)
{
IEnumerable<SearchResultEntity> results = _treeSearcher.ExamineSearch(query, UmbracoEntityTypes.Media, pageSize,
pageIndex, out var totalFound, searchFrom);
IEnumerable<SearchResultEntity> results = _treeSearcher.ExamineSearch(
query,
UmbracoEntityTypes.Media,
pageSize,
pageIndex,
out var totalFound,
searchFrom: searchFrom);
return new EntitySearchResults(results, totalFound);
}
@@ -51,7 +51,13 @@ public class MemberTreeController : TreeController, ISearchableTree, ITreeNodeCo
public async Task<EntitySearchResults> SearchAsync(string query, int pageSize, long pageIndex, string? searchFrom = null)
{
IEnumerable<SearchResultEntity> results = _treeSearcher.ExamineSearch(query, UmbracoEntityTypes.Member, pageSize, pageIndex, out var totalFound, searchFrom);
IEnumerable<SearchResultEntity> results = _treeSearcher.ExamineSearch(
query,
UmbracoEntityTypes.Member,
pageSize,
pageIndex,
out var totalFound,
searchFrom: searchFrom);
return new EntitySearchResults(results, totalFound);
}
@@ -0,0 +1,12 @@
using Microsoft.AspNetCore.Http;
using Umbraco.Cms.Core.DeliveryApi;
namespace Umbraco.Cms.Web.Common.Accessors;
public sealed class RequestContextOutputExpansionStrategyAccessor : RequestContextServiceAccessorBase<IOutputExpansionStrategy>, IOutputExpansionStrategyAccessor
{
public RequestContextOutputExpansionStrategyAccessor(IHttpContextAccessor httpContextAccessor)
: base(httpContextAccessor)
{
}
}
@@ -0,0 +1,20 @@
using System.Diagnostics.CodeAnalysis;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
namespace Umbraco.Cms.Web.Common.Accessors;
public abstract class RequestContextServiceAccessorBase<T>
where T : class
{
private readonly IHttpContextAccessor _httpContextAccessor;
protected RequestContextServiceAccessorBase(IHttpContextAccessor httpContextAccessor)
=> _httpContextAccessor = httpContextAccessor;
public bool TryGetValue([NotNullWhen(true)] out T? requestStartNodeService)
{
requestStartNodeService = _httpContextAccessor.HttpContext?.RequestServices.GetService<T>();
return requestStartNodeService is not null;
}
}
@@ -211,7 +211,7 @@ public static class FriendlyPublishedContentExtensions
/// </param>
/// <returns></returns>
/// <remarks>
/// This can be useful in order to return all nodes in an entire site by a type when combined with TypedContentAtRoot
/// This can be useful in order to return all nodes in an entire site by a type when combined with ContentAtRoot
/// </remarks>
public static IEnumerable<IPublishedContent> DescendantsOrSelfOfType(
this IEnumerable<IPublishedContent> parentNodes, string docTypeAlias, string? culture = null)
@@ -227,7 +227,7 @@ public static class FriendlyPublishedContentExtensions
/// </param>
/// <returns></returns>
/// <remarks>
/// This can be useful in order to return all nodes in an entire site by a type when combined with TypedContentAtRoot
/// This can be useful in order to return all nodes in an entire site by a type when combined with ContentAtRoot
/// </remarks>
public static IEnumerable<T> DescendantsOrSelf<T>(
this IEnumerable<IPublishedContent> parentNodes,
@@ -0,0 +1,148 @@
using Umbraco.Cms.Core.DeliveryApi;
using Umbraco.Cms.Core.Models.PublishedContent;
using Umbraco.Extensions;
namespace Umbraco.Cms.Web.Common.Rendering;
public class ElementOnlyOutputExpansionStrategy : IOutputExpansionStrategy
{
protected const string All = "$all";
protected const string None = "";
protected const string ExpandParameterName = "expand";
protected const string FieldsParameterName = "fields";
private readonly IApiPropertyRenderer _propertyRenderer;
protected Stack<Node?> ExpandProperties { get; } = new();
protected Stack<Node?> IncludeProperties { get; } = new();
public ElementOnlyOutputExpansionStrategy(
IApiPropertyRenderer propertyRenderer)
{
_propertyRenderer = propertyRenderer;
}
public virtual IDictionary<string, object?> MapContentProperties(IPublishedContent content)
=> content.ItemType == PublishedItemType.Content
? MapProperties(content.Properties)
: throw new ArgumentException($"Invalid item type. This method can only be used with item type {nameof(PublishedItemType.Content)}, got: {content.ItemType}");
public virtual IDictionary<string, object?> MapMediaProperties(IPublishedContent media, bool skipUmbracoProperties = true)
{
if (media.ItemType != PublishedItemType.Media)
{
throw new ArgumentException($"Invalid item type. This method can only be used with item type {PublishedItemType.Media}, got: {media.ItemType}");
}
IPublishedProperty[] properties = media
.Properties
.Where(p => skipUmbracoProperties is false || p.Alias.StartsWith("umbraco") is false)
.ToArray();
return properties.Any()
? MapProperties(properties)
: new Dictionary<string, object?>();
}
public virtual IDictionary<string, object?> MapElementProperties(IPublishedElement element)
=> MapProperties(element.Properties, true);
private IDictionary<string, object?> MapProperties(IEnumerable<IPublishedProperty> properties, bool forceExpandProperties = false)
{
Node? currentExpandProperties = ExpandProperties.Count > 0 ? ExpandProperties.Peek() : null;
if (ExpandProperties.Count > 1 && currentExpandProperties is null && forceExpandProperties is false)
{
return new Dictionary<string, object?>();
}
Node? currentIncludeProperties = IncludeProperties.Count > 0 ? IncludeProperties.Peek() : null;
var result = new Dictionary<string, object?>();
foreach (IPublishedProperty property in properties)
{
Node? nextIncludeProperties = GetNextProperties(currentIncludeProperties, property.Alias);
if (currentIncludeProperties is not null && currentIncludeProperties.Items.Any() && nextIncludeProperties is null)
{
continue;
}
Node? nextExpandProperties = GetNextProperties(currentExpandProperties, property.Alias);
IncludeProperties.Push(nextIncludeProperties);
ExpandProperties.Push(nextExpandProperties);
result[property.Alias] = GetPropertyValue(property);
ExpandProperties.Pop();
IncludeProperties.Pop();
}
return result;
}
private Node? GetNextProperties(Node? currentProperties, string propertyAlias)
=> currentProperties?.Items.FirstOrDefault(i => i.Key == All)
?? currentProperties?.Items.FirstOrDefault(i => i.Key == "properties")?.Items.FirstOrDefault(i => i.Key == All || i.Key == propertyAlias);
private object? GetPropertyValue(IPublishedProperty property)
=> _propertyRenderer.GetPropertyValue(property, ExpandProperties.Peek() is not null);
protected sealed class Node
{
public string Key { get; private set; } = string.Empty;
public List<Node> Items { get; } = new();
public static Node Parse(string value)
{
// verify that there are as many start brackets as there are end brackets
if (value.CountOccurrences("[") != value.CountOccurrences("]"))
{
throw new ArgumentException("Value did not contain an equal number of start and end brackets");
}
// verify that the value does not start with a start bracket
if (value.StartsWith("["))
{
throw new ArgumentException("Value cannot start with a bracket");
}
// verify that there are no empty brackets
if (value.Contains("[]"))
{
throw new ArgumentException("Value cannot contain empty brackets");
}
var stack = new Stack<Node>();
var root = new Node { Key = "root" };
stack.Push(root);
var currentNode = new Node();
root.Items.Add(currentNode);
foreach (char c in value)
{
switch (c)
{
case '[': // Start a new node, child of the current node
stack.Push(currentNode);
currentNode = new Node();
stack.Peek().Items.Add(currentNode);
break;
case ',': // Start a new node, but at the same level of the current node
currentNode = new Node();
stack.Peek().Items.Add(currentNode);
break;
case ']': // Back to parent of the current node
currentNode = stack.Pop();
break;
default: // Add char to current node key
currentNode.Key += c;
break;
}
}
return root;
}
}
}
@@ -14,6 +14,6 @@ public class ConfigureFormOptions : IConfigureOptions<FormOptions>
// convert from KB to bytes
options.MultipartBodyLengthLimit = _runtimeSettings.Value.MaxRequestLength.HasValue
? _runtimeSettings.Value.MaxRequestLength.Value * 1024
? (long)_runtimeSettings.Value.MaxRequestLength.Value * 1024
: long.MaxValue;
}
@@ -15,6 +15,6 @@ public class ConfigureKestrelServerOptions : IConfigureOptions<KestrelServerOpti
// convert from KB to bytes, 52428800 bytes (50 MB) is the same as in the IIS settings
options.Limits.MaxRequestBodySize = _runtimeSettings.Value.MaxRequestLength.HasValue
? _runtimeSettings.Value.MaxRequestLength.Value * 1024
? (long)_runtimeSettings.Value.MaxRequestLength.Value * 1024
: 52428800;
}
+2
View File
@@ -349,6 +349,8 @@ public class UmbracoHelper
public IEnumerable<IPublishedContent> ContentAtRoot() => _publishedContentQuery.ContentAtRoot();
public IEnumerable<IPublishedContent> ContentAtRoot(string? culture) => _publishedContentQuery.ContentAtRoot(culture);
#endregion
#region Media
+26 -21
View File
@@ -24,9 +24,9 @@
"angular-sanitize": "1.8.3",
"angular-touch": "1.8.3",
"angular-ui-sortable": "0.19.0",
"animejs": "3.2.1",
"animejs": "3.2.2",
"bootstrap-social": "5.1.1",
"chart.js": "^2.9.3",
"chart.js": "^2.9.4",
"clipboard": "2.0.11",
"diff": "5.1.0",
"flatpickr": "4.6.13",
@@ -37,12 +37,12 @@
"lazyload-js": "1.0.0",
"moment": "2.29.4",
"ng-file-upload": "12.2.13",
"nouislider": "15.7.1",
"nouislider": "15.7.2",
"spectrum-colorpicker2": "2.0.10",
"tinymce": "6.8.2",
"tinymce": "6.8.6",
"typeahead.js": "0.11.1",
"underscore": "1.13.6",
"wicg-inert": "3.1.2"
"underscore": "1.13.7",
"wicg-inert": "3.1.3"
},
"devDependencies": {
"@babel/core": "7.21.8",
@@ -3360,9 +3360,10 @@
}
},
"node_modules/animejs": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/animejs/-/animejs-3.2.1.tgz",
"integrity": "sha512-sWno3ugFryK5nhiDm/2BKeFCpZv7vzerWUcUPyAZLDhMek3+S/p418ldZJbJXo5ZUOpfm2kP2XRO4NJcULMy9A=="
"version": "3.2.2",
"resolved": "https://registry.npmjs.org/animejs/-/animejs-3.2.2.tgz",
"integrity": "sha512-Ao95qWLpDPXXM+WrmwcKbl6uNlC5tjnowlaRYtuVDHHoygjtIPfDUoK9NthrlZsQSKjZXlmji2TrBUAVbiH0LQ==",
"license": "MIT"
},
"node_modules/ansi-colors": {
"version": "1.1.0",
@@ -12971,9 +12972,10 @@
}
},
"node_modules/nouislider": {
"version": "15.7.1",
"resolved": "https://registry.npmjs.org/nouislider/-/nouislider-15.7.1.tgz",
"integrity": "sha512-5N7C1ru/i8y3dg9+Z6ilj6+m1EfabvOoaRa7ztpxBSKKRZso4vA52DGSbBJjw5XLtFr/LZ9SgGAXqyVtlVHO5w=="
"version": "15.7.2",
"resolved": "https://registry.npmjs.org/nouislider/-/nouislider-15.7.2.tgz",
"integrity": "sha512-Jn1xuJZk0cs7XdzFD2tKucedlvi5/eEAsyewPS70Qqba3a1vcyEey2VZFmp7JiS06QfgkD0kEFk8oMnJrLDWXQ==",
"license": "MIT"
},
"node_modules/now-and-later": {
"version": "2.0.1",
@@ -16694,9 +16696,10 @@
"integrity": "sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q=="
},
"node_modules/tinymce": {
"version": "6.8.2",
"resolved": "https://registry.npmjs.org/tinymce/-/tinymce-6.8.2.tgz",
"integrity": "sha512-Lho79o2Y1Yn+XdlTEkHTEkEmzwYWTXz7IUsvPwxJF3VTtgHUIAAuBab29kik+f2KED3rZvQavr9D7sHVMJ9x4A=="
"version": "6.8.6",
"resolved": "https://registry.npmjs.org/tinymce/-/tinymce-6.8.6.tgz",
"integrity": "sha512-++XYEs8lKWvZxDCjrr8Baiw7KiikraZ5JkLMg6EdnUVNKJui0IsrAADj5MsyUeFkcEryfn2jd3p09H7REvewyg==",
"license": "MIT"
},
"node_modules/to-absolute-glob": {
"version": "2.0.2",
@@ -17026,9 +17029,10 @@
}
},
"node_modules/underscore": {
"version": "1.13.6",
"resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.6.tgz",
"integrity": "sha512-+A5Sja4HP1M08MaXya7p5LvjuM7K6q/2EaC0+iovj/wOcMsTzMvDFbasi/oSapiwOlt252IqsKqPjCl7huKS0A=="
"version": "1.13.7",
"resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.7.tgz",
"integrity": "sha512-GMXzWtsc57XAtguZgaQViUOzs0KTkk8ojr3/xAxXLITqf/3EMwxC0inyETfDFjH/Krbhuep0HNbbjI9i/q3F3g==",
"license": "MIT"
},
"node_modules/undertaker": {
"version": "1.3.0",
@@ -17738,9 +17742,10 @@
"dev": true
},
"node_modules/wicg-inert": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/wicg-inert/-/wicg-inert-3.1.2.tgz",
"integrity": "sha512-Ba9tGNYxXwaqKEi9sJJvPMKuo063umUPsHN0JJsjrs2j8KDSzkWLMZGZ+MH1Jf1Fq4OWZ5HsESJID6nRza2ang=="
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/wicg-inert/-/wicg-inert-3.1.3.tgz",
"integrity": "sha512-5L0PKK7iP+0Q/jv2ccgmkz/pfXbumZtlEyWS/xnX+L+Og3f7WjL4+iEs18k4IuldOX3PgGpza3qGndL9xUBjCQ==",
"license": "W3C-20150513"
},
"node_modules/word-wrap": {
"version": "1.2.4",

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