Compare commits

...
Author SHA1 Message Date
Niels Lyngsø 42ae5f1ac9 Merge branch 'release/16.0' into v16/feature/context-proxy
# Conflicts:
#	src/Umbraco.Web.UI.Client/src/packages/core/repository/detail/detail-repository-base.ts
#	src/Umbraco.Web.UI.Client/src/packages/core/tree/default/default-tree.element.ts
2025-04-24 21:24:54 +02:00
Niels Lyngsø 5260aab5c8 implement UmbContextProxy 2025-04-24 21:22:47 +02:00
Niels Lyngsø 3d47dda810 use it self as host 2025-04-24 20:38:49 +02:00
mole ef9a6e1821 Merge remote-tracking branch 'origin/v15/dev' into v16/dev 2025-04-24 13:19:21 +02:00
b4528cf963 Fixed error with reflection on integration test configure builder attributes, so integration tests can be created outside of the Umbraco integration test project (#19077)
* Fixed error with reflection on integration test configure builder attributes, so integration tests can be created outside of the Umbraco integration test project.

* Fix nullability

---------

Co-authored-by: mole <nikolajlauridsen@protonmail.ch>
2025-04-24 13:18:29 +02:00
cb1ec988ce V15: Ensure elements cache is cleared on subscribers in load balanced scenarios (#19128)
* Clear elementscache from cache refreshers

* Add very simple test ensuring the elements cache is cleared

---------

Co-authored-by: Kenn Jacobsen <kja@umbraco.dk>
2025-04-24 13:15:30 +02:00
Kenn JacobsenandGitHub 8caf4ae3e0 Retrofit ContentEditingBaseBuilder to match new create/update models (#19129) 2025-04-24 12:28:05 +02:00
Warren BuckleyandMads Rasmussen 97ded97fc7 Add 'ManifestWithDynamicConditions' to ManifestHeaderApp so Header Apps can be conditionally shown/loaded (#19124) 2025-04-24 12:26:58 +02:00
b48d989659 V16: Removes all generic Created, Saved, and Deleted notifications (#19119)
* feat: removes all generic Created, Saved, and Deleted notifications

* Comment out the notification checks in acceptance tests

* Bumped version of test helper

* Fixed publish with descendants tests

* Cleaned up

* Bumped version of test helper

---------

Co-authored-by: Nhu Dinh <hnd@umbraco.dk>
Co-authored-by: Nhu Dinh <150406148+nhudinh0309@users.noreply.github.com>
Co-authored-by: Niels Lyngsø <nsl@umbraco.dk>
2025-04-24 11:50:49 +02:00
Sven Geusensandkjac 297ceaf0aa Remove fake null checks as they are no longer needed after merge into v16 (#19109) 2025-04-24 11:46:41 +02:00
Jacob OvergaardandGitHub 1e99d6f609 V16: Adds texts for unpublish & publish with descendants (#19118)
* feat: adds new texts for 'unpublished' action

* chore(mock): adds missing endpoints for mock data

* feat: removes old, deprecated, and hardcoded messages for unpublish

* fix: publish actions should not fail just because the notification context might not be available

* feat: the unpublish actions should mimick the publish actions

* chore(mock): adds mock endpoints for 'publish with descendants'

* feat: moves 'publish with descendants' notifications to calling workspace

* feat: adds DK translations

* feat: adds different notification for unpublishing invariant content
2025-04-24 11:32:53 +02:00
Jacob OvergaardandGitHub b8c2a3366f V16: Bumps core dependencies openapi-ts and uuid to latest (#19127)
* removes `server-api-dev` script that acts weird with base urls after upgrade of client-fetch

* build(deps): updates client-fetch and uuid dependencies
2025-04-24 11:11:36 +02:00
6d330a30eb V16: Context Api can unprovide Contexts (#19113)
* make consume return undefined

* make consume return undefined

* a few more undefined context handlings

* unprovide context

* rename

* jsdocs

* refactor UmbContextBase to not use generic types

* reset target on disconnect

* posible undefined context

* callback with undefined when disconnected

* update comment

* correct types

* correct error handling

* do not throw an error when missing

* always return permitted to onChange callback

* fix not existing store

* fix resetting structure manager

* fix requestAuditLogs

* support gone context

* support context not begin present

* use UMB_ENTITY_WORKSPACE_CONTEXT for right typing

* correct type to use UMB_SUBMITTABLE_WORKSPACE_CONTEXT

* correct context consumption

* fix tests

* fix tests

* catch modal registration that has been destroyed

* catch

* handle context unprovide

* more clean up

* fix context consumption

* Update repository-details.manager.ts

* enable store to be undefined

* enable UmbRelationTypeDetailRepository store to be undefined

* remove log

---------

Co-authored-by: Mads Rasmussen <madsr@hey.com>
2025-04-24 08:57:02 +00:00
Jacob Overgaard 607207b13b chore: updates OpenApi.json 2025-04-24 10:34:49 +02:00
Kenn JacobsenandGitHub 1dd5fe3cbc Ensure the default segment is always included in API responses (#19126) 2025-04-24 09:46:30 +02:00
Mads Rasmussen 275478cd93 Merge branch 'v16/dev' into v16/feature/consume-can-be-undefined 2025-04-24 08:54:57 +02:00
Niels LyngsøandGitHub 80bf2314c6 upgrade client dependencies (#19121)
* update npm

* run icon script

* generate server end-points
2025-04-24 08:52:26 +02:00
Niels LyngsøandGitHub 8342d4b94f minor styling adjustments (#19122) 2025-04-24 08:51:17 +02:00
Niels LyngsøandGitHub 148935a3fc Merge branch 'v16/dev' into v16/feature/consume-can-be-undefined 2025-04-24 07:59:13 +02:00
Mads RasmussenandGitHub 60eaef5590 Fix UI creation and updating of Document Blueprints (#19123)
* fallback to save preselected variants

* Update content-detail-workspace-base.ts
2025-04-24 07:58:08 +02:00
Andy ButlandandGitHub 3dd32642e2 Updated server-side dependencies to latest versions for Umbraco 16 (#19117)
* Updated dependencies to latest versions.

* Fixed breaking changes following dependency updates.

* Limited NUnit updates to within the current major.

* Fixed failing delivery API contract integration test.
2025-04-23 20:05:41 +02:00
Niels Lyngsø b69ab3ad78 make tree store optional 2025-04-23 19:54:41 +02:00
Mads RasmussenandGitHub 513f980e5c Split "Sort Children"-logic to remove "illegal" import from core to packages (#19105)
* wip sortChildrenOfContent kind

* export types

* add modal token + consts

* Update manifests.ts

* add content tree item model

* wip use umb-table element

* set as prop

* render sort icon

* prevent selection when sortable

* remove unused

* clean up

* reflect sortable prop

* start implementing sortChildrenOfContent

* render name and create date

* handle date ordering

* remove unused

* clean up

* fix grab and grabbing styling for sortable table rows

* render label when no children

* Update sort-children-of-content-modal.element.ts

* fix styling of load more

* only allow sorting when all items are loaded

* Update index.js
2025-04-23 15:12:48 +00:00
024a450377 Added integration tests for publishing service with invalid content (#19095)
* Added integration tests for publishing service with invalid content.

* Amend test to new create/update models

---------

Co-authored-by: Kenn Jacobsen <kja@umbraco.dk>
2025-04-23 15:09:39 +00:00
Niels LyngsøandGitHub 9246ecbb08 reject init when document-type failed to load (#19092) 2025-04-23 14:21:30 +00:00
Niels Lyngsø 5adc4702a8 remove log 2025-04-23 16:02:49 +02:00
Niels Lyngsø 5b54e8e469 enable UmbRelationTypeDetailRepository store to be undefined 2025-04-23 16:02:29 +02:00
Niels Lyngsø 5b05094fa2 enable store to be undefined 2025-04-23 16:01:27 +02:00
Mads Rasmussen af004230b2 Update repository-details.manager.ts 2025-04-23 15:15:38 +02:00
Mads RasmussenandGitHub a917858265 Merge branch 'v16/dev' into v16/feature/consume-can-be-undefined 2025-04-23 15:10:12 +02:00
b330395db0 Content Type inheritance (#19034)
* content type nesting

* TODOs

* repository detail manager

* todo

* implement unlimited compositions

* a little refactor

* warn

* clear state

* refactor to use unique

* note

* code corrections to match with types

* unique type for Array State

* implement usedForInheritance and editedTypes for Structure Manager and Compositions

* rename method

* Update repository-details.manager.ts

* avoid type casting

* align naming

* do not await

* fix race condition when switching document types fast

* remove test prop

* Update manifests.ts

* UmbMediaTypeWorkspaceContext Routes for inheritance

* import

---------

Co-authored-by: Mads Rasmussen <madsr@hey.com>
2025-04-23 15:05:32 +02:00
2cf28271cd Service refactoring to "fully" enable segments (#19114)
* Refactor serverside content editing to support all variance combinations

* Fix build errors

* Reintroduce the tests ignored by #19060

---------

Co-authored-by: Mads Rasmussen <madsr@hey.com>
2025-04-23 14:54:51 +02:00
Niels Lyngsø a3848077b4 fix context consumption 2025-04-23 14:37:14 +02:00
Niels Lyngsø 3fc1c3afee more clean up 2025-04-23 14:30:40 +02:00
Niels LyngsøandGitHub f181c2f82b Merge branch 'v16/dev' into v16/feature/consume-can-be-undefined 2025-04-23 13:44:23 +02:00
53b94a8f57 Add async methods to IFileType (#19116)
* Add GetChecksumStreamAsync and GetLengthAsync

* Obsolete CanSetPhysical, Set and GetVirtualPath

* Updated obsolete messages to reference Umbraco 18

---------

Co-authored-by: Andy Butland <abutland73@gmail.com>
2025-04-23 11:38:03 +00:00
bdb7baf946 Add report for bidirectional module imports (#19112)
* add report for bidirectional module imports

* remove duplicates

---------

Co-authored-by: Niels Lyngsø <nsl@umbraco.dk>
2025-04-23 11:35:35 +00:00
b898eb6e03 Removed population of Urls on document response model and obsoleted property (#19030)
* Removed population of Urls on document response model and obsoleted property.

* Updated readme for acceptance tests to show how to run a single test.

* Removed URLs from document models on the client-side and fixed issue with link picker stil using legacy URLs response data.

---------

Co-authored-by: Nikolaj Geisle <70372949+Zeegaan@users.noreply.github.com>
Co-authored-by: Kenn Jacobsen <kja@umbraco.dk>
2025-04-23 10:58:38 +00:00
Niels LyngsøandGitHub 3187d99d23 Custom validator example take2 (#19093)
* declare type and constant

* implement for example

* commit example

* fix document data

* make rte blocks optional

* remove blocks from this mock data

* fix mock data for RTE

* comment and destroy method implementation

* set to 8

* update comments

* remove console.log

* host may be undefined

* prevent duplicate messages
2025-04-23 12:47:19 +02:00
Andy ButlandandGitHub 7c98af558d Allow for configuration of log file names (#19074)
* Added configuration for the log file name and format.

* Added unit test for LoggingConfiguration.

* Rely on configuration validation to verify supported log file format arguments.

* Fixed unit test failing on build pipeline.
2025-04-23 12:28:51 +02:00
Lee KelleherandGitHub 1bddcb8d39 Imports umb-app-logo (#19103)
for displaying the backoffice logo
2025-04-23 10:11:36 +00:00
Niels Lyngsø e4424ff0b5 handle context unprovide 2025-04-23 12:03:10 +02:00
Niels Lyngsø 339c852ec2 Merge branch 'v16/dev' into v16/feature/consume-can-be-undefined
# Conflicts:
#	src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-split-view/workspace-split-view-variant-selector.element.ts
2025-04-23 11:28:06 +02:00
Niels Lyngsø c0bd937216 catch 2025-04-23 11:27:09 +02:00
Niels Lyngsø 7062042cbc catch modal registration that has been destroyed 2025-04-23 11:21:37 +02:00
Niels Lyngsø af0abf252a fix tests 2025-04-23 11:21:24 +02:00
Niels Lyngsø d9a0f3af02 fix tests 2025-04-23 11:10:17 +02:00
Niels LyngsøandGitHub bb3b8b2255 Merge pull request #19060 from umbraco/v15/feature/select-segment
Feature: Segments
2025-04-23 10:55:05 +02:00
Niels Lyngsø 60af1e88f0 Merge branch 'v16/dev' into v16/feature/consume-can-be-undefined 2025-04-23 10:34:35 +02:00
Niels Lyngsø 0ea16b41b8 correct context consumption 2025-04-23 10:30:57 +02:00
Niels Lyngsø ba33de49e7 correct type to use UMB_SUBMITTABLE_WORKSPACE_CONTEXT 2025-04-23 10:29:25 +02:00
Niels Lyngsø ac81973e3b use UMB_ENTITY_WORKSPACE_CONTEXT for right typing 2025-04-23 10:26:36 +02:00
85b58f2015 Custom serialization for ValidationProblemDetails to allign paths in custom and mvc error messages (#19111)
* Rowrked #18771 with a more brute force solution

* Fixes class name. Adds unit test verifying behaviour.

* Corrected test name.

---------

Co-authored-by: Kenn Jacobsen <kja@umbraco.dk>
Co-authored-by: Andy Butland <abutland73@gmail.com>
2025-04-23 10:17:18 +02:00
Niels Lyngsø 8fe9599e91 support context not begin present 2025-04-23 10:12:16 +02:00
Niels Lyngsø e017167f57 support gone context 2025-04-23 10:12:08 +02:00
Niels Lyngsø 79ea5c12c1 fix requestAuditLogs 2025-04-23 10:09:48 +02:00
d568e981ab V16: Implement cache tags (#19101)
* Implement tags for content cache

* Implement tags for media cache

* Refactor to only use cache and media tags

* Remove from DI

* Cleanup

* Update Nuget packages

* Apply suggestions from code review

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

* Change description to be more precise

* Minor code tidy: indents, static methods where possible, made tags methods a little terser.

* Fixed according to review

---------

Co-authored-by: Elitsa <elm@umbraco.dk>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Andy Butland <abutland73@gmail.com>
2025-04-23 10:08:08 +02:00
Niels LyngsøandGitHub 808e56beec Merge branch 'v16/dev' into v15/feature/select-segment 2025-04-23 10:07:37 +02:00
Niels Lyngsø c171882d55 fix resetting structure manager 2025-04-23 10:06:59 +02:00
Andreas Zerbst 85cba7e8cb Updated locator 2025-04-23 10:02:41 +02:00
Andreas Zerbst dcf601b44e Bumped version of testHelpers 2025-04-23 10:02:26 +02:00
Niels Lyngsø 20a3e08ae0 fix not existing store 2025-04-23 09:59:44 +02:00
Niels Lyngsø 049fad3261 always return permitted to onChange callback 2025-04-23 09:55:30 +02:00
Niels Lyngsø ede48c1b8b Merge branch 'v16/dev' into v16/feature/consume-can-be-undefined 2025-04-23 09:38:53 +02:00
Niels Lyngsø 4114342de2 add observe alias 2025-04-23 09:38:10 +02:00
Niels Lyngsø 1c93d1e6e1 Merge branch 'v16/dev' into v16/feature/consume-can-be-undefined 2025-04-23 09:36:19 +02:00
Niels Lyngsø 3c6cb488d7 do not throw an error when missing 2025-04-23 09:35:41 +02:00
Niels Lyngsø 33ed2482d8 correct error handling 2025-04-23 09:35:29 +02:00
Niels Lyngsø 056656042b correct types 2025-04-23 09:35:19 +02:00
Niels Lyngsø e3eec6ce1f update comment 2025-04-23 09:35:08 +02:00
Niels LyngsøandGitHub 5b76d3bc04 Merge branch 'v16/dev' into v15/feature/select-segment 2025-04-23 09:32:21 +02:00
Niels LyngsøandGitHub 18a8e3bde5 implement readonly mode for umb-property-editor-ui-document-type-picker (#19089) 2025-04-23 07:12:56 +00:00
Niels LyngsøandGitHub 8849647dcd declare type and constant (#19091) 2025-04-23 07:08:08 +00:00
fcddba5c73 Update version of MiniProfiler.AspNetCore.Mvc to align with MiniProfiler.Shared. (#19107)
Co-authored-by: Kenn Jacobsen <kja@umbraco.dk>
2025-04-23 07:59:51 +02:00
64f1e17479 Segment support for the Delivery API (#19082)
* Segment support for the Delivery API

* Do not apply empty strings as variance.

---------

Co-authored-by: Niels Lyngsø <nsl@umbraco.dk>
2025-04-23 05:45:04 +00:00
Kenn JacobsenandGitHub 4a768b694e Merge branch 'v16/dev' into v15/feature/select-segment 2025-04-23 07:12:07 +02:00
Sven GeusensandGitHub c42237ecfa Remove fake null checks as they are no longer needed after merge into v16 (#19109) 2025-04-23 07:09:00 +02:00
Niels Lyngsø 37c9d20d60 callback with undefined when disconnected 2025-04-22 22:11:34 +02:00
Niels Lyngsø f2a22bc51b posible undefined context 2025-04-22 22:11:22 +02:00
Niels Lyngsø 3baf6faed4 reset target on disconnect 2025-04-22 22:04:26 +02:00
Niels Lyngsø a4453243e6 refactor UmbContextBase to not use generic types 2025-04-22 21:57:32 +02:00
Niels Lyngsø a24058a0ab jsdocs 2025-04-22 20:42:22 +02:00
Niels Lyngsø 548b14610c rename 2025-04-22 19:59:52 +02:00
Niels Lyngsø bca39401ac unprovide context 2025-04-22 16:48:44 +02:00
Niels Lyngsø b072a4ab60 a few more undefined context handlings 2025-04-22 16:48:37 +02:00
Niels Lyngsø fca17b87da Merge branch 'v16/dev' into v16/feature/consume-can-be-undefined 2025-04-22 16:11:42 +02:00
Mads Rasmussen b794392df8 Delete Segments.cs 2025-04-22 15:16:49 +02:00
Mads Rasmussen 09d1615e60 only show culture options when unpublishing 2025-04-22 15:06:07 +02:00
Mads Rasmussen 7d4d8c9a79 Update document-publishing.workspace-context.ts 2025-04-22 15:01:46 +02:00
Mads Rasmussen 78877bea57 correct variant selection in save modal 2025-04-22 14:44:16 +02:00
kjac 8ef061b768 Ignore failing tests for now (will be fixed in a follow-up PR) 2025-04-22 14:13:43 +02:00
kjac 3521549ff3 Fix build errors 2025-04-22 14:10:45 +02:00
Niels Lyngsø 39046c35af refactor to remove 2025-04-22 14:08:06 +02:00
Niels Lyngsø b4f26a8ffe style "open-split-view" 2025-04-22 14:01:43 +02:00
Mads Rasmussen bdabaa80d3 Update workspace-split-view-variant-selector.element.ts 2025-04-22 13:47:01 +02:00
Mads Rasmussen 1f63e7d41b Update workspace-split-view-variant-selector.element.ts 2025-04-22 13:45:14 +02:00
Niels Lyngsø 366e8d6809 html refactor + restyling 2025-04-22 13:38:22 +02:00
Mads Rasmussen bcafffdbf0 enforce allowNonExistingSegmentsCreation config 2025-04-22 13:31:20 +02:00
Mads Rasmussen f725ea8019 remove todo 2025-04-22 12:33:29 +02:00
Kenn JacobsenandGitHub 548970a27f Merge branch 'v16/dev' into v15/feature/select-segment 2025-04-22 12:07:04 +02:00
Mads Rasmussen 6c6e1f7336 do not render toggle if there are no segment variant options 2025-04-22 11:22:03 +02:00
Andy Butland 1dc9f8540d Merge branch 'v16/dev' of https://github.com/umbraco/Umbraco-CMS into v16/dev 2025-04-22 10:01:05 +02:00
Andy Butland 6987515cbe Merge remote-tracking branch 'origin/v15/dev' into v16/dev
# Conflicts:
#	version.json
2025-04-22 09:52:59 +02:00
Andy Butland 8e87f2260c Bumped version in dev branch to 15.5.0-rc. 2025-04-22 09:52:10 +02:00
Mads Rasmussen 2fc5880d7d Update property-workspace-view-settings.element.ts 2025-04-22 09:47:26 +02:00
Andy Butland 927e4d8cad Merge branch 'release/15.4' into v15/dev 2025-04-22 09:45:15 +02:00
Niels LyngsøandGitHub fb39711571 give tiptap a invalid border color to fit with other inputs in invalid state (#19090) 2025-04-22 08:43:12 +01:00
Andy Butland 225f4d3e09 Merge remote-tracking branch 'origin/v15/dev' into v16/dev 2025-04-22 09:33:07 +02:00
Mads Rasmussen a290601e5e fix property shared labels in content type workspace 2025-04-22 09:30:58 +02:00
Niels Lyngsø 0b93561a06 make consume return undefined 2025-04-21 23:34:45 +02:00
Niels Lyngsø 01ea259d74 make consume return undefined 2025-04-21 23:25:43 +02:00
Niels Lyngsø 141a4a4678 Improve error 2025-04-21 20:24:58 +02:00
Niels Lyngsø 74a9b8aae7 JSDocs for Validation Context 2025-04-21 20:24:17 +02:00
Sven GeusensandMigaroez c38faec74b V15/fix/sub variant block deletion (#18802)
* Fix

* Editors with access should be able to clear a blocklist value

* Writeup around block element level variation

* Dissallow values to be removed a limited language user does not have permissions to

* Remove commented out code

* improved comments

* Improve expose list for limited language access sub variant block lists
2025-04-21 10:37:14 +02:00
Sven GeusensandGitHub c86a6fa8e5 V15/fix/sub variant block deletion (#18802)
* Fix

* Editors with access should be able to clear a blocklist value

* Writeup around block element level variation

* Dissallow values to be removed a limited language user does not have permissions to

* Remove commented out code

* improved comments

* Improve expose list for limited language access sub variant block lists
2025-04-21 10:30:12 +02:00
Lee KelleherandGitHub f69f257d12 Refines umb-localize rendering (#19075) 2025-04-17 14:45:40 +02:00
b1795da0ee Adds searchResultItem element for Media items (#19058)
* Adds `searchResultItem` for Media

Includes "trashed" state tag

* Removes trailing whitespace from manifest name

---------

Co-authored-by: Andy Butland <abutland73@gmail.com>
2025-04-17 07:57:19 +00:00
Lee KelleherandGitHub 38af101605 Adds search to Document Type picker (#19062)
* Loosens the guard for an empty `query`

* Adds search to Document Type picker

* Document Type Search Provider: adds support for querying element types

* Adds `pickerSearchResultItem` element for Document Types
2025-04-17 09:25:06 +02:00
Lee KelleherandGitHub aa89eed3aa V16: Chore: Search modal code sweep (#19061)
* Removes trailing whitespace from manifest names

* Refactored default `umb-search-result-item` element

- Reverted `elementName` constant
- Replaced fallback hashtag inline SVG with "icon-shape-hexagon"
- Added "extra" div (for UI consistency)
- Reworked CSS rules

* Search modal: replaced inline icons

* Search modal: refactored markup

* Search modal: removed font-size

for consistency with the rest of the backoffice.
2025-04-17 09:24:32 +02:00
Lee KelleherandGitHub bb41647d91 Dynamic Root: refactor to use UMB_ANCESTORS_ENTITY_CONTEXT (#19032) 2025-04-17 09:23:49 +02:00
235a951f31 Transparent Focal Point (#19047)
* make focal point transparent

* Update src/Umbraco.Web.UI.Client/src/packages/media/media/components/input-image-cropper/image-cropper-focus-setter.element.ts

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

* remove background color

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-04-17 09:22:52 +02:00
Lee KelleherandGitHub 804995e59e Content Picker Search: Option to exclude trashed items (#19064)
* Document Picker: support for excluding trashed items

* Media Picker: support for excluding trashed items
2025-04-17 09:21:10 +02:00
Lee KelleherandGitHub 5544823b97 Adds searchResultItem for Document Type (#19059)
Includes "Element Type" tag.
2025-04-17 09:20:12 +02:00
Lee KelleherandGitHub b9cf156e5a Update community forum links (#19065)
* Help menu: fixes Our Umbraco link

* Changes forum link from Our Umbraco

to the new Community Forum website.

* Small fixes in the Log Viewer Message

* Help Menu: flattens structure

for tighter UI.

* Replaced hot-linked favicon images

with built-in icons.
Except for Microsoft Bing.

* Corrected "Search Umbraco Forum using Google" link
2025-04-17 09:17:25 +02:00
Lee KelleherandGitHub 2fcf1c7812 Adds trashed/draft state to document search result items (#19057)
* Corrected class name

* Reverted `elementName` constant

* Replaced `UmbAppLanguageContext` type usage

with `typeof UMB_APP_LANGUAGE_CONTEXT.TYPE`.

* Refactored `render` method

- "Trashed", adds tag + faded text + strike-through
- "Draft", adds tag + faded text
- Replaced fallback hashtag icon with "icon-document"
- Reworked CSS rules
2025-04-17 09:16:30 +02:00
Niels LyngsøandGitHub a9043a543e align validation with new fetch client (#19071) 2025-04-17 09:15:54 +02:00
Niels LyngsøandGitHub cd83cb2852 Merge branch 'v16/dev' into v15/feature/select-segment 2025-04-16 22:39:31 +02:00
Niels Lyngsø b2ab769edc change varying labels in Content Type Designer 2025-04-16 22:01:46 +02:00
Mads RasmussenandGitHub 59cad092ae Cleanup last known self imports (#19067)
* clean up self imports in workspace module

* clean up media type self imports

* fix self imports in property action module

* fix self imports in propety editor module

* Update index.js

* lower threshold
2025-04-16 19:33:25 +00:00
Niels Lyngsø 70bdabf9d6 clean up unused parts 2025-04-16 20:56:10 +02:00
Niels Lyngsø 3030006e4f correct error message 2025-04-16 20:48:32 +02:00
Andy Butlandandleekelleher 1e521760df Removes tree actions from tree selection in duplicate document dialog. (#19053)
(cherry picked from commit 1ccb5cc09e)
2025-04-16 18:23:26 +01:00
Andy ButlandandGitHub 1ccb5cc09e Removes tree actions from tree selection in duplicate document dialog. (#19053) 2025-04-16 18:21:39 +01:00
Niels LyngsøandGitHub 4561394283 Merge branch 'v16/dev' into v15/feature/select-segment 2025-04-16 18:05:26 +02:00
Mads Rasmussen 6038428f79 rename 2025-04-16 15:21:11 +02:00
Mads Rasmussen d52d3aa642 add todo 2025-04-16 15:18:07 +02:00
Mads Rasmussen 20d763f443 render toggle if segmentation is enabled in the config 2025-04-16 15:17:00 +02:00
Jacob Overgaard c63ed69ee2 Merge branch 'v15/dev' into v16/dev 2025-04-16 14:54:50 +02:00
Jacob Overgaard 12ca71558a Merge branch 'release/15.4' into v15/dev 2025-04-16 14:48:09 +02:00
Jacob Overgaard 8b759cf3a4 bump version to 15.4.0-rc2 2025-04-16 14:47:55 +02:00
Jacob Overgaard 25fffd4173 Merge remote-tracking branch 'origin/release/15.4' into v15/dev 2025-04-16 14:47:02 +02:00
29c0151460 Scaffold content for content templates serverside (#19054)
* Scaffold content for content templates serverside

* Generated client types and methods from API.

* Retrieve scaffolded blueprint when creating documents from a blueprint.

* Use introduced helper method on existing read.

* Cleaned up imports.

* feat: moves scaffold service logic to data source and make shallow repository method

* feat: follows UmbDataSourceResponse interface and reorders public/private methods

* Bumped version to 15.4.0-r2.

---------

Co-authored-by: Andy Butland <abutland73@gmail.com>
Co-authored-by: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com>
2025-04-16 12:39:07 +00:00
Lee KelleherandGitHub 2b6f9fb9c4 V16: Chore: Deprecate getManifest() methods (#19056)
* Deprecates `getManifest()` methods

Resolution is to use the `.manifest` property instead.

* Revised the deprecation message to improve clarity
2025-04-16 14:35:50 +02:00
Lee KelleherandGitHub 0eac3952a6 Code sweep (#19055)
Markup and style tidy-up and consistency.
Import ordering.
2025-04-16 14:34:49 +02:00
Mads Rasmussen b87e8d90e4 remove unused code 2025-04-16 14:33:09 +02:00
Jacob Overgaard 889bd0b6e4 fix a bad merge 2025-04-16 14:07:42 +02:00
Mads Rasmussen 6033daff3b add label for shared cultures and segments 2025-04-16 13:44:10 +02:00
Lee KelleherandGitHub 4cec19625f Corrects UmbLocalizationController usage in coments/docs (#19052) 2025-04-16 13:15:25 +02:00
Mads Rasmussen cc25027e65 Update element-data-manager.ts 2025-04-16 12:31:32 +02:00
Mads Rasmussen f91c0568d1 temp remove 2025-04-16 12:31:24 +02:00
Mads Rasmussen d391cff254 move 2025-04-16 11:22:49 +02:00
Mads Rasmussen 63485bca15 Update content-data-manager.ts 2025-04-16 10:49:11 +02:00
Mads Rasmussen 64915eea92 Update content-detail-workspace-base.ts 2025-04-16 10:49:05 +02:00
Mads Rasmussen fbdf69e680 Update content-data-manager.ts 2025-04-16 10:25:56 +02:00
Mads Rasmussen 13109d7342 use new tryExecute 2025-04-16 09:39:23 +02:00
Mads Rasmussen e988c5a450 move fields to variant interface 2025-04-16 09:33:40 +02:00
Mads Rasmussen eddf06e773 Merge branch 'v16/dev' into v15/feature/select-segment 2025-04-16 09:29:28 +02:00
761c76143d Make the entity search service async (#19048)
* Make the entity search service async

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

---------

Co-authored-by: Andy Butland <abutland73@gmail.com>
2025-04-15 14:20:31 +00:00
Kenn JacobsenandGitHub 96c0509719 Content search abstractions to facilitate new search in the backoffice (#19046) 2025-04-15 16:18:42 +02:00
Mads RasmussenandGitHub fcc8e2fe3a Chore: Temp solution to remove user circular imports (#19021)
* remove user and document/media circular imports

* fix code order

* change MAX_CIRCULAR_DEPENDENCIES

* remove all comments from content before checking for imports

* fix self import

* fix self imports
2025-04-15 13:18:14 +00:00
Jacob Overgaard dab3975d2e Merge branch 'v15/dev' into v16/dev 2025-04-15 14:47:51 +02:00
Migaroez beb60065bb Merge branch 'release/15.4' into v15/dev 2025-04-15 14:40:19 +02:00
Jacob Overgaard cc9c33bfe6 fix: adds missing export 2025-04-15 12:57:57 +02:00
Mads RasmussenandGitHub 3cd3abe5c8 Fix document module self import (#19035)
* fix document self import

* lower SELF_IMPORTS_THRESHOLD

* Update index.js

* Update index.js
2025-04-15 10:46:33 +00:00
Jacob Overgaard 6edffd9f08 docs: fix import path 2025-04-15 12:23:37 +02:00
Mads RasmussenandGitHub e6ab1d9a6b Move + Rename input-collection-configuration element to input-content-type-collection-configuration (#19041)
* move to content-type module

* make name more explicit

* add total to text

* update threshold

* remove completed todo
2025-04-15 09:27:06 +00:00
Mads RasmussenandGitHub 37e25c3b2b fix for member and member type (#19037) 2025-04-15 10:48:19 +02:00
Mads RasmussenandGitHub bfa9cab1cf Update theme.context.ts (#19036) 2025-04-15 10:47:58 +02:00
4087f3fab7 V16: Split media handling from UmbDropzoneManager (#19031)
* move media handling into separate class

* override manager + remove deprecated code

* remove export

* fix order

* fix export of consts

* Update src/Umbraco.Web.UI.Client/src/packages/media/media/dropzone/media-dropzone.manager.ts

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

* fix imports

* fix wrong dropzone manager instance

* remove unused

* remove unused

* lower MAX_CIRCULAR_DEPENDENCIES

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-04-15 09:52:05 +02:00
Jacob Overgaard e9b0486988 Merge branch 'v15/dev' into v16/dev 2025-04-15 09:43:56 +02:00
Jacob Overgaard 7e57396960 Merge branch 'contrib' into v15/dev 2025-04-15 09:21:11 +02:00
Andy Butland 981f173a79 Clarified "too many" entries validation message. (#19040) 2025-04-15 08:35:49 +02:00
Andy ButlandandGitHub 83fe87ca88 Clarified "too many" entries validation message. (#19040) 2025-04-15 06:34:58 +00:00
Andy Butland 432dda8c47 Adds some missing mime types to ensure uploaded audio and video displays with preview. (#19039) 2025-04-15 08:04:11 +02:00
Andy ButlandandGitHub ae078bf96b Adds some missing mime types to ensure uploaded audio and video displays with preview. (#19039) 2025-04-15 08:03:10 +02:00
6455f96e51 CONTRIBUTING.md - fix incorrect link to contributing-first-issue.md (#19038)
Co-authored-by: Andrew Lacey <AndrewL@if.co.nz>
2025-04-15 06:35:10 +02:00
Mads RasmussenandGitHub 906285120e Dont check for imports in comments (#19026)
* remove all comments from content before checking for imports

* lower treshold
2025-04-14 18:50:20 +00:00
Andy Butlandandkjac a9fc88d7e6 Make sure not to early return when verifying ancestor path is published without completing scope (#19029)
* Make sure not to early return when verifying ancestor path is published without completing scope.

* Added comment.
2025-04-14 17:46:57 +02:00
Andy ButlandandGitHub 76e2103551 Make sure not to early return when verifying ancestor path is published without completing scope (#19029)
* Make sure not to early return when verifying ancestor path is published without completing scope.

* Added comment.
2025-04-14 17:45:57 +02:00
340ae6c474 Document 3rd Party Licenses (#19027)
* specifies the license for base64-js

* specifies the license for router-slot

* specifies the license for contentment/bytes

* adds a general third-party notices file for all direct dependency libraries

* merge separate licenses for icon packs into NOTICES.txt

* updates lee kelleher permission

* updates the name of DOMPurify

* adds Element Internals Polyfill

* capitalize names

* Update src/Umbraco.Web.UI.Client/src/packages/core/utils/bytes/bytes.function.ts

Co-authored-by: Lee Kelleher <leekelleher@users.noreply.github.com>

* adds babel

* adds eslint

* adds hey-api/openapi-ts

* adds open web components

* adds playwright

* adds rollup

* adds storybook

* adds cross-env

* adds glob

* adds globals

* adds madge

* adds msw

* adds playwright-msw

* adds prettier

* adds remark-gfm

* adds svgo

* adds tiny-glob

* adds tsc-alias

* adds typedoc

* adds typescript

* adds typescript-eslint

* adds typescript-json-schema

* adds vite-plugin-static-copy

* adds vite-tsconfig-paths

* adds web component analyzer

* adds rollup-plugin-esbuild

* adds rollup-plugin-import-css

* adds rollup plugins as general license

---------

Co-authored-by: Lee Kelleher <leekelleher@users.noreply.github.com>
2025-04-14 16:01:14 +02:00
Kenn JacobsenandGitHub 7495c3c7b2 Treat content schedule dates as UTC (#19028) 2025-04-14 14:50:02 +02:00
Sebastiaan Janssen 449c94f08d Merge branch 'v15/dev' into contrib 2025-04-14 14:22:36 +02:00
83b92666ac Use maximum available value for JSON serialization depth (#19001)
* Use maximum available value for JSON serialization depth.

* Updated unittests with best practices

---------

Co-authored-by: Migaroez <geusens@gmail.com>
2025-04-14 13:50:19 +02:00
Henrik GedionsenandSebastiaan Janssen b7bf30744a Use GetIdsFromPathReversed to avoid allocating the string values of the integers parsed 2025-04-14 11:19:08 +02:00
dependabot[bot]andJacob Overgaard 9e97f6028b Bump vite from 6.2.5 to 6.2.6 in /src/Umbraco.Web.UI.Client
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 6.2.5 to 6.2.6.
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/v6.2.6/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v6.2.6/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  dependency-version: 6.2.6
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-04-14 11:01:59 +02:00
f9496e8067 Ensure dates read from the database are treated as local when constructing entities (2) (#19013)
* Revert "Ensure dates read from the database are treated as local when constructing entities (#18989)"

This reverts commit 7b10d39d66.

* Avoid system dates stored with local server time being defaulted to UTC on database read.

* ContentScheduleDto.Date is UTC

---------

Co-authored-by: kjac <kja@umbraco.dk>
2025-04-14 10:55:36 +02:00
dependabot[bot]andJacob Overgaard 8ba3a4d926 Bump vite from 6.2.5 to 6.2.6 in /src/Umbraco.Web.UI.Login
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 6.2.5 to 6.2.6.
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/v6.2.6/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v6.2.6/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  dependency-version: 6.2.6
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-04-14 10:22:22 +02:00
Nhu DinhandGitHub 7fe6971a61 V15 Updated acceptance tests for unsupported invariant content item with variant blocks (#18919)
* Updated tests for invariant block grid with variant blocks

* Updated tests for invariant block list with variant blocks

* Updated tests for invariant document with variant blocks

* Bumped version

* Changed npm command

* Updated isValidationMessageVisible

* Fixed name

* Fixed comments

* Reverted npm command
2025-04-12 08:59:31 +00:00
Andy Butlandandmole 7b10d39d66 Ensure dates read from the database are treated as local when constructing entities (#18989)
* Ensure dates read from the database are treated as local when constructing entities.

* Fixed typos in comments.
2025-04-11 13:19:13 +02:00
Andy ButlandandGitHub 9ef2b37be4 Ensure dates read from the database are treated as local when constructing entities (#18989)
* Ensure dates read from the database are treated as local when constructing entities.

* Fixed typos in comments.
2025-04-11 09:32:05 +02:00
Nhu DinhandGitHub 516d62a348 V15 Added acceptance tests for the mandatory property editor in a content or a block (#18920)
* Added tests for mandatory checkboxlist in a content

* Added tests for mandatory dropdown in a content

* Added tests for mandatory media picker in a content

* Added tests for mandatory radiobox in a content

* Added tests for block grid with mandatory property editors

* Added tests for block list with mandatory property editors

* Bumped version

* Cleaned up

* Changed npm command

* Fixed comments

* Fixed comments

* Reverted
2025-04-11 03:13:36 +00:00
Niels LyngsøandGitHub 97b3023e14 make sure only to prepend relative URLs (#18998) 2025-04-10 14:56:14 +01:00
Mads Rasmussen 250ddf319e only show the local split view 2025-04-01 15:40:58 +02:00
Mads Rasmussen 015c0cfc16 indent segments 2025-04-01 15:39:21 +02:00
Mads Rasmussen 53be19cb00 auto expand parent culture 2025-04-01 15:33:10 +02:00
Mads Rasmussen 3a93c9b9a4 set segment variant name 2025-04-01 14:44:02 +02:00
Mads Rasmussen ff39e54ec6 adjust display name 2025-04-01 14:42:31 +02:00
Mads Rasmussen 5a6d7326d8 render segment name in input 2025-04-01 13:27:18 +02:00
Mads Rasmussen c8e9beffa1 set name input to readonly for segments 2025-04-01 13:16:21 +02:00
Mads Rasmussen e064169df5 more explicit method name 2025-04-01 13:11:19 +02:00
Mads Rasmussen d87415099c align icon 2025-04-01 13:01:10 +02:00
Mads Rasmussen 34cfa3cb62 only render segments if culture is created 2025-04-01 12:48:32 +02:00
Mads Rasmussen 9e9b1efdf3 only show culture variant options in dialogs 2025-04-01 11:03:02 +02:00
Mads Rasmussen f309117223 nest segments under cultures 2025-04-01 10:53:42 +02:00
Mads Rasmussen 0ab024d7c1 remove duplicate tag 2025-04-01 10:17:37 +02:00
Mads Rasmussen 302b58e513 Merge branch 'v15/dev' into v15/feature/select-segment 2025-04-01 10:08:46 +02:00
kjac 8d9822ea70 Fix mapping errors for invariant editing 2025-03-31 07:06:23 +02:00
kjac 15f0f44ff5 Fix segment variant mapping from API 2025-03-30 14:05:07 +02:00
kjac 043b39d553 Allow for creating and editing segment variant documents 2025-03-30 13:02:00 +02:00
Mads Rasmussen 2ff11afa95 only show button ellipsis when there are culture variants 2025-03-28 10:15:24 +01:00
Mads Rasmussen e61541a2cf fix type errors 2025-03-28 10:00:48 +01:00
Mads Rasmussen d6b40ce526 send segments when saving a culture 2025-03-28 09:43:52 +01:00
Mads Rasmussen f63794737a use variant id class to check 2025-03-28 08:29:39 +01:00
Mads Rasmussen 9e5c7977da check for null segment for invariant option 2025-03-28 08:27:47 +01:00
Mads Rasmussen 64eb3cdf57 fix spelling mistake 2025-03-28 08:21:25 +01:00
Mads Rasmussen 6b60ff8ade remove default language tag 2025-03-28 08:06:02 +01:00
Mads Rasmussen e1e69aeec4 handle segmented data when saving 2025-03-28 08:02:53 +01:00
Mads Rasmussen 746ad55a0b fix name display values 2025-03-27 14:43:59 +01:00
Mads Rasmussen cb33afba3b support segments for route fallback 2025-03-27 14:13:37 +01:00
Mads Rasmussen 98fd90409e return empty array 2025-03-27 13:39:54 +01:00
Mads Rasmussen e8ba63bad4 only execute if we have proper values 2025-03-27 13:37:16 +01:00
Mads Rasmussen 5557ec4cd8 remove log 2025-03-27 11:35:32 +01:00
Mads Rasmussen dbf0e740d8 render label based on how to document varies 2025-03-27 11:34:40 +01:00
Mads Rasmussen 7a21cf5617 add observables to interface 2025-03-27 11:34:18 +01:00
Mads Rasmussen c21d160f52 render segment name 2025-03-27 10:59:50 +01:00
Mads Rasmussen b873288015 do not include segments in save options 2025-03-27 10:34:06 +01:00
Mads Rasmussen 2264ce8678 use alias in spec info 2025-03-27 10:23:00 +01:00
Mads Rasmussen f296d5d2b4 render name instead of alias 2025-03-27 10:16:49 +01:00
Mads Rasmussen bc9cc83eca Merge branch 'v15/feature/property-vary-by-segment' into v15/feature/select-segment 2025-03-26 19:47:16 +01:00
Mads Rasmussen 17c4a08ae1 Merge branch 'v15/dev' into v15/feature/property-vary-by-segment 2025-03-26 19:46:31 +01:00
Mads Rasmussen f1020436d1 only show default language tag for a language 2025-03-26 15:26:49 +01:00
Mads Rasmussen b21183c562 fix types 2025-03-26 15:20:59 +01:00
Mads Rasmussen 6d974a624d Merge branch 'v15/feature/property-vary-by-segment' into v15/feature/select-segment 2025-03-26 15:18:10 +01:00
Mads Rasmussen 6ba4b36514 Merge branch 'v15/dev' into v15/feature/property-vary-by-segment 2025-03-26 15:17:51 +01:00
Mads Rasmussen 30f1ec194e include segment in active variants 2025-03-26 15:14:37 +01:00
Mads Rasmussen 2801df3c6f cast type 2025-03-26 15:14:15 +01:00
Mads Rasmussen 666f9457f3 Update content-detail-workspace-base.ts 2025-03-26 13:40:02 +01:00
Mads Rasmussen 6e42c53d79 add culture for segments 2025-03-26 13:34:10 +01:00
Mads Rasmussen 7e713cab84 append segment to spec info 2025-03-26 13:29:51 +01:00
Mads Rasmussen 0ebe465e6f add segment info to model 2025-03-26 13:25:38 +01:00
Mads Rasmussen d583169642 make variant options for segments too 2025-03-26 11:51:43 +01:00
Mads Rasmussen 721e6ccb0c clean up 2025-03-25 21:48:51 +01:00
Mads Rasmussen 45ff3c3a6f wip loading segment data 2025-03-25 21:42:29 +01:00
Mads Rasmussen 21a23b8057 render tag for vary by segment 2025-03-25 20:33:26 +01:00
Mads Rasmussen 04d8b29226 remove duplicates 2025-03-25 20:30:27 +01:00
Mads Rasmussen f76d093226 simplify labels 2025-03-25 20:30:20 +01:00
Mads Rasmussen b3055fa5ce use property-layout for appearance 2025-03-25 20:24:16 +01:00
Mads Rasmussen 7f089a630a render toggle for vary by segment 2025-03-25 20:20:15 +01:00
839 changed files with 13272 additions and 6447 deletions
+1 -1
View File
@@ -61,7 +61,7 @@ This guide describes each step to make your first contribution:
## Further contribution guides
- [Before you start](contributing-before-you-start.md)
- [Finding your first issue: Up for grabs](contributing-before-you-start.md)
- [Finding your first issue: Up for grabs](contributing-first-issue.md)
- [Contributing to the new backoffice](https://docs.umbraco.com/umbraco-backoffice/)
- [Unwanted changes](contributing-unwanted-changes.md)
- [Other ways to contribute](contributing-other-ways-to-contribute.md)
+45 -46
View File
@@ -5,99 +5,98 @@
</PropertyGroup>
<!-- Global packages (private, build-time packages for all projects) -->
<ItemGroup>
<GlobalPackageReference Include="Nerdbank.GitVersioning" Version="3.6.146" />
<GlobalPackageReference Include="Nerdbank.GitVersioning" Version="3.7.115" />
<GlobalPackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.556" />
<GlobalPackageReference Include="Umbraco.Code" Version="2.2.0" />
<GlobalPackageReference Include="Umbraco.GitVersioning.Extensions" Version="0.2.0" />
</ItemGroup>
<!-- Microsoft packages -->
<ItemGroup>
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation" Version="9.0.0" />
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="4.10.0" />
<PackageVersion Include="Microsoft.Data.Sqlite" Version="9.0.2" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="9.0.2" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.SqlServer" Version="9.0.2" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.10" />
<PackageVersion Include="Microsoft.Extensions.Caching.Abstractions" Version="9.0.2" />
<PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="9.0.2" />
<PackageVersion Include="Microsoft.Extensions.Configuration.Abstractions" Version="9.0.2" />
<PackageVersion Include="Microsoft.Extensions.Configuration.Json" Version="9.0.2" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="9.0.2" />
<PackageVersion Include="Microsoft.Extensions.FileProviders.Embedded" Version="9.0.2" />
<PackageVersion Include="Microsoft.Extensions.FileProviders.Physical" Version="9.0.2" />
<PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="9.0.2" />
<PackageVersion Include="Microsoft.Extensions.Http" Version="9.0.2" />
<PackageVersion Include="Microsoft.Extensions.Identity.Core" Version="9.0.2" />
<PackageVersion Include="Microsoft.Extensions.Identity.Stores" Version="9.0.2" />
<PackageVersion Include="Microsoft.Extensions.Logging" Version="9.0.2" />
<PackageVersion Include="Microsoft.Extensions.Options" Version="9.0.2" />
<PackageVersion Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="9.0.2" />
<PackageVersion Include="Microsoft.Extensions.Options.DataAnnotations" Version="9.0.2" />
<PackageVersion Include="Microsoft.Extensions.Caching.Hybrid" Version="9.0.0-preview.9.24556.5" />
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation" Version="9.0.4" />
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="4.10.0" /> <!-- TODO: Update the hard-dependency that Umbraco.Code has on 4.10.0 to allow update of this to latest (4.13.0). -->
<PackageVersion Include="Microsoft.Data.Sqlite" Version="9.0.4" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="9.0.4" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.SqlServer" Version="9.0.4" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.4" />
<PackageVersion Include="Microsoft.Extensions.Caching.Abstractions" Version="9.0.4" />
<PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="9.0.4" />
<PackageVersion Include="Microsoft.Extensions.Configuration.Abstractions" Version="9.0.4" />
<PackageVersion Include="Microsoft.Extensions.Configuration.Json" Version="9.0.4" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="9.0.4" />
<PackageVersion Include="Microsoft.Extensions.FileProviders.Embedded" Version="9.0.4" />
<PackageVersion Include="Microsoft.Extensions.FileProviders.Physical" Version="9.0.4" />
<PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="9.0.4" />
<PackageVersion Include="Microsoft.Extensions.Http" Version="9.0.4" />
<PackageVersion Include="Microsoft.Extensions.Identity.Core" Version="9.0.4" />
<PackageVersion Include="Microsoft.Extensions.Identity.Stores" Version="9.0.4" />
<PackageVersion Include="Microsoft.Extensions.Logging" Version="9.0.4" />
<PackageVersion Include="Microsoft.Extensions.Options" Version="9.0.4" />
<PackageVersion Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="9.0.4" />
<PackageVersion Include="Microsoft.Extensions.Options.DataAnnotations" Version="9.0.4" />
<PackageVersion Include="Microsoft.Extensions.Caching.Hybrid" Version="9.4.0" />
</ItemGroup>
<!-- Umbraco packages -->
<ItemGroup>
<PackageVersion Include="Umbraco.JsonSchema.Extensions" Version="0.3.0" />
<PackageVersion Include="Umbraco.CSharpTest.Net.Collections" Version="15.0.0" />
</ItemGroup>
<!-- Third-party packages -->
<ItemGroup>
<PackageVersion Include="Asp.Versioning.Mvc" Version="8.1.0" />
<PackageVersion Include="Asp.Versioning.Mvc.ApiExplorer" Version="8.1.0" />
<PackageVersion Include="Dazinator.Extensions.FileProviders" Version="2.0.0" />
<PackageVersion Include="Examine" Version="3.7.0" />
<PackageVersion Include="Examine.Core" Version="3.7.0" />
<PackageVersion Include="HtmlAgilityPack" Version="1.11.74" />
<PackageVersion Include="JsonPatch.Net" Version="3.1.1" />
<PackageVersion Include="Examine" Version="3.7.1" />
<PackageVersion Include="Examine.Core" Version="3.7.1" />
<PackageVersion Include="HtmlAgilityPack" Version="1.12.1" />
<PackageVersion Include="JsonPatch.Net" Version="3.3.0" />
<PackageVersion Include="K4os.Compression.LZ4" Version="1.3.8" />
<PackageVersion Include="MailKit" Version="4.10.0" />
<PackageVersion Include="MailKit" Version="4.11.0" />
<PackageVersion Include="Markdown" Version="2.2.1" />
<PackageVersion Include="MessagePack" Version="2.5.192" />
<PackageVersion Include="MiniProfiler.AspNetCore.Mvc" Version="4.3.8" />
<PackageVersion Include="MessagePack" Version="3.1.3" />
<PackageVersion Include="MiniProfiler.AspNetCore.Mvc" Version="4.5.4" />
<PackageVersion Include="MiniProfiler.Shared" Version="4.5.4" />
<PackageVersion Include="ncrontab" Version="3.3.3" />
<PackageVersion Include="NPoco" Version="5.7.1" />
<PackageVersion Include="NPoco.SqlServer" Version="5.7.1" />
<PackageVersion Include="OpenIddict.Abstractions" Version="6.1.1" />
<PackageVersion Include="OpenIddict.AspNetCore" Version="6.1.1" />
<PackageVersion Include="OpenIddict.EntityFrameworkCore" Version="6.1.1" />
<PackageVersion Include="OpenIddict.Abstractions" Version="6.2.1" />
<PackageVersion Include="OpenIddict.AspNetCore" Version="6.2.1" />
<PackageVersion Include="OpenIddict.EntityFrameworkCore" Version="6.2.1" />
<PackageVersion Include="Serilog" Version="4.2.0" />
<PackageVersion Include="Serilog.AspNetCore" Version="8.0.3" />
<PackageVersion Include="Serilog.AspNetCore" Version="9.0.0" />
<PackageVersion Include="Serilog.Enrichers.Process" Version="3.0.0" />
<PackageVersion Include="Serilog.Enrichers.Thread" Version="4.0.0" />
<PackageVersion Include="Serilog.Expressions" Version="5.0.0" />
<PackageVersion Include="Serilog.Extensions.Hosting" Version="8.0.0" />
<PackageVersion Include="Serilog.Extensions.Hosting" Version="9.0.0" />
<PackageVersion Include="Serilog.Formatting.Compact" Version="3.0.0" />
<PackageVersion Include="Serilog.Formatting.Compact.Reader" Version="4.0.0" />
<PackageVersion Include="Serilog.Settings.Configuration" Version="8.0.4" />
<PackageVersion Include="Serilog.Settings.Configuration" Version="9.0.0" />
<PackageVersion Include="Serilog.Sinks.Async" Version="2.1.0" />
<PackageVersion Include="Serilog.Sinks.File" Version="6.0.0" />
<PackageVersion Include="Serilog.Sinks.Map" Version="2.0.0" />
<PackageVersion Include="SixLabors.ImageSharp" Version="3.1.7" />
<PackageVersion Include="SixLabors.ImageSharp.Web" Version="3.1.3" />
<PackageVersion Include="Swashbuckle.AspNetCore" Version="7.1.0" />
<PackageVersion Include="SixLabors.ImageSharp.Web" Version="3.1.4" />
<PackageVersion Include="Swashbuckle.AspNetCore" Version="8.1.1" />
</ItemGroup>
<!-- Transitive pinned versions (only required because our direct dependencies have vulnerable versions of transitive dependencies) -->
<ItemGroup>
<!-- Microsoft.EntityFrameworkCore.SqlServer and NPoco.SqlServer brings in a vulnerable version of Azure.Identity -->
<!-- Take top-level depedendency on Azure.Identity, because Microsoft.EntityFrameworkCore.SqlServer depends on a vulnerable version -->
<PackageVersion Include="Azure.Identity" Version="1.13.1" />
<PackageVersion Include="Azure.Identity" Version="1.13.2" />
<!-- Microsoft.EntityFrameworkCore.SqlServer brings in a vulnerable version of System.Runtime.Caching -->
<PackageVersion Include="System.Runtime.Caching" Version="9.0.0" />
<PackageVersion Include="System.Runtime.Caching" Version="9.0.4" />
<!-- Dazinator.Extensions.FileProviders brings in a vulnerable version of System.Net.Http -->
<PackageVersion Include="System.Net.Http" Version="4.3.4" />
<!-- Examine brings in a vulnerable version of System.Security.Cryptography.Xml -->
<PackageVersion Include="System.Security.Cryptography.Xml" Version="9.0.0" />
<PackageVersion Include="System.Security.Cryptography.Xml" Version="9.0.4" />
<!-- Dazinator.Extensions.FileProviders and MiniProfiler.AspNetCore.Mvc brings in a vulnerable version of System.Text.RegularExpressions -->
<PackageVersion Include="System.Text.RegularExpressions" Version="4.3.1" />
<!-- OpenIddict.AspNetCore, Npoco.SqlServer and Microsoft.EntityFrameworkCore.SqlServer brings in a vulnerable version of Microsoft.IdentityModel.JsonWebTokens -->
<!-- Take top-level depedendency on Microsoft.IdentityModel.JsonWebTokens, because OpenIddict.AspNetCore, Npoco.SqlServer and Microsoft.EntityFrameworkCore.SqlServer depends on a vulnerable version -->
<PackageVersion Include="Microsoft.IdentityModel.JsonWebTokens" Version="8.2.1" />
<PackageVersion Include="Microsoft.IdentityModel.JsonWebTokens" Version="8.8.0" />
<!-- Azure.Identity, Microsoft.EntityFrameworkCore.SqlServer and Dazinator.Extensions.FileProviders brings in a legacy version of System.Text.Encodings.Web -->
<PackageVersion Include="System.Text.Encodings.Web" Version="9.0.0" />
<PackageVersion Include="System.Text.Encodings.Web" Version="9.0.4" />
<!-- NPoco.SqlServer brings in a vulnerable version of Microsoft.Data.SqlClient -->
<PackageVersion Include="Microsoft.Data.SqlClient" Version="5.2.2" />
<PackageVersion Include="Microsoft.Data.SqlClient" Version="6.0.1" />
<!-- Examine.Lucene brings in a vulnerable version of Lucene.Net.Replicator -->
<PackageVersion Include="Lucene.Net.Replicator" Version="4.8.0-beta00017" />
</ItemGroup>
</Project>
</Project>
+571
View File
@@ -0,0 +1,571 @@
Third-Party Notices
===================
This file contains notices and attributions for third-party software used in the Umbraco CMS project.
It is not a license and does not grant any rights to use the third-party software.
Umbraco CMS is licensed under the MIT License, which can be found in the LICENSE file.
---
@openid/AppAuth-JS: An OpenID Connect and OAuth 2.0 client library for JavaScript
URL: https://github.com/openid/AppAuth-JS
License: Apache License, Version 2.0
Copyright: 2017 Google Inc.
---
AutoFixture: Write maintainable unit tests, faster
URL: https://github.com/AutoFixture/AutoFixture
License: MIT License
Copyright: 2013 Mark Seemann
---
Asp.Versioning.Mvc: A library for ASP.NET Core versioning
URL: https://github.com/dotnet/aspnet-api-versioning
License: MIT License
Copyright: .NET Foundation and contributors
---
Babel: A JavaScript compiler
URL: https://babeljs.io/
License: MIT License
Copyright: 2014-present Sebastian McKenzie and other contributors
---
BenchmarkDotNet: Powerful .NET library for benchmarking
URL: https://github.com/dotnet/BenchmarkDotNet
License: MIT License
Copyright: .NET Foundation and Contributors
---
Bogus: A simple and sane data generator for populating objects that supports different locales.
URL: https://github.com/bchavez/Bogus
License: MIT License
Copyright: 2015 Brian Chavez
---
CommandLineParser: Terse syntax C# command line parser for .NET
URL: https://github.com/commandlineparser/commandline
License: MIT License
Copyright: 2005-2015 Giacomo Stelluti Scala & Contributors
---
cross-env: A CLI tool to set environment variables across platforms
URL: https://github.com/kentcdodds/cross-env
License: MIT License
Copyright: 2017 Kent C. Dodds
---
Dazinator.Extensions.FileProviders: A library for file provider extensions
URL: https://github.com/dazinator/Dazinator.Extensions.FileProviders
License: MIT License
Copyright: 2016 Darrell
---
DOMPurify: A DOM-only XSS sanitizer for HTML, MathML and SVG
URL: https://github.com/cure53/DOMPurify
License: Apache License, Version 2.0
Copyright: 2025 Dr.-Ing. Mario Heiderich, Cure53
---
Element Internals Polyfill: A polyfill for the Element Internals API
URL: https://github.com/calebdwilliams/element-internals-polyfill
License: MIT License
Copyright: 2021 Caleb Williams
---
Eslint: A tool for identifying and reporting on patterns in JavaScript
URL: https://eslint.org/
License: MIT License
Copyright: OpenJS Foundation and other contributors
---
Examine: A search and indexing library for .NET
URL: https://github.com/Shazwazza/Examine
License: Microsoft Public License (Ms-PL)
Copyright: 2023 Shannon Deminick
---
Glob: A library for matching file paths using glob patterns
URL: https://github.com/isaacs/node-glob
License: ISC License
Copyright: 2009-2023 Isaac Z. Schlueter and Contributors
---
Globals: A library for managing global variables in JavaScript
URL: https://github.com/sindresorhus/globals
License: MIT License
Copyright: Sindre Sorhus
---
Html Agility Pack: An HTML parser for .NET
URL: https://html-agility-pack.net/
License: MIT License
Copyright: ZZZ Projects Inc.
---
ImageSharp: A cross-platform library for processing images in .NET
URL: https://github.com/SixLabors/ImageSharp
License: Apache License, Version 2.0 under the Six Labors Split License
Copyright: Six Labors
---
jsdiff: A JavaScript text differencing implementation
URL: https://github.com/kpdecker/jsdiff
License: BSD 3-Clause License
Copyright: 2009-2015 Kevin Decker <kpdecker@gmail.com>
---
JsonPatch.Net: A library for JSON Patch (RFC 6902) in .NET
URL: https://github.com/json-everything/json-everything
License: MIT License
Copyright: .NET Foundation and Contributors
---
K4os.Compression.LZ4: A fast LZ4 compression library for .NET
URL: https://github.com/MiloszKrajewski/K4os.Compression.LZ4
License: MIT License
Copyright: 2017 Milosz Krajewski
---
Lit: A simple library for building fast, lightweight web components
URL: https://lit.dev
License: BSD 3-Clause License
Copyright: 2020 Google LLC. All rights reserved.
---
Lucide: Beautiful & consistent icons for the web
URL: https://lucide.dev/
License: ISC License
Copyright: 2013-2022 Cole Bemis
Copyright: 2022 Lucide Contributors
---
Madge: A dependency graph generator for JavaScript
URL: https://github.com/pahen/madge
License: MIT License
Copyright: 2017 Patrik Henningsson
---
MailKit: A library for sending email in .NET
URL: https://github.com/jstedfast/MailKit
License: MIT License
Copyright: 2013-2024 .NET Foundation and Contributors
---
Markdown: A library for parsing and compiling Markdown
URL: https://github.com/hey-red/Markdown
License: MIT License
Copyright: 2018 red
---
marked: A markdown parser and compiler
URL: https://marked.js.org/
License: MIT License
Copyright: 2011-2018, Christopher Jeffrey (https://github.com/chjj/)
Copyright: 2018+, MarkedJS (https://github.com/markedjs/)
---
Message Pack: The extremely fast MessagePack serializer for C#
URL: https://github.com/MessagePack-CSharp/MessagePack-CSharp
License: MIT License
Copyright: 2017 Yoshifumi Kawai and contributors
---
Miniprofiler: A mini profiler for .NET
URL: https://github.com/MiniProfiler/dotnet
License: MIT License
Copyright: .NET MiniProfiler Contributors
---
Monaco Editor: A browser-based code editor
URL: https://microsoft.github.io/monaco-editor/
License: MIT License
Copyright: 2016-present Microsoft Corporation
---
Moq: A mocking library for .NET
URL: https://github.com/moq/moq
License: BSD 3-Clause License
Copyright: 2007 Clarius Consulting, Manas Technology Solutions, InSTEDD, and Contributors.
---
Mock Service Worker (MSW): A library for mocking API requests in JavaScript
URL: https://mswjs.io/
License: MIT License
Copyright: 2018present Artem Zakharchenko
---
NCrontab: A cron schedule parser for .NET
URL: https://github.com/atifaziz/NCrontab
License: Apache License, Version 2.0
Copyright: 2001 The OpenSymphony Group
Copyright: 2008 Atif Aziz
---
Nerdbank.GitVersioning: A library for versioning .NET projects
URL: https://github.com/dotnet/Nerdbank.GitVersioning
License: MIT License
Copyright: .NET Foundation and Contributors
---
NJsonSchema: A JSON schema validator for .NET
URL: https://github.com/RicoSuter/NJsonSchema
License: MIT License
Copyright: 2022 Rico Suter
---
NPoco: A micro ORM for .NET
URL: https://github.com/schotime/NPoco
License: Apache License, Version 2.0
Copyright: Schotime
---
NUnit: A unit testing framework for .NET
URL: https://github.com/nunit/nunit
License: MIT License
Copyright: Charlie Poole, Rob Prouse and Contributors
---
Open Web Components: A set of standards and libraries for building web components
URL: https://open-wc.org/
License: MIT License
Copyright: 2018 open-wc
---
Openapi-ts: The OpenAPI to TypeScript codegen
URL: https://github.com/hey-api/openapi-ts
License: MIT License
Copyright: Hey API
---
OpenIddict: A simple and flexible OpenID Connect server for ASP.NET Core
URL: https://github.com/openiddict/openiddict-core
License: Apache License, Version 2.0
Copyright: Kévin Chalet
---
Playwright: A Node.js library to automate browser testing
URL: https://playwright.dev/
License: Apache License, Version 2.0
Copyright: 2025 Microsoft Corporation
---
Playwright-msw: A library to wrap Mock Service Worker with Playwright
URL: https://github.com/valendres/playwright-msw
License: MIT License
Copyright: 2022 Peter Weller
---
Prettier: An opinionated code formatter
URL: https://prettier.io/
License: MIT License
Copyright: James Long and contributors
---
Remark-gfm: A GitHub Flavored Markdown plugin for Remark
URL: https://github.com/remarkjs/remark-gfm
License: MIT License
Copyright: Titus Wormer
---
Rollup: A module bundler for JavaScript
URL: https://rollupjs.org/
License: MIT License
Copyright: 2015-present Rollup contributors
---
Rollup Plugins: A collection of Rollup plugins
URL: https://github.com/rollup/plugins
License: MIT License
Copyright: 2019-present Rollup Plugins contributors
---
Rollup-plugin-esbuild: A Rollup plugin for using esbuild
URL: https://github.com/egoist/rollup-plugin-esbuild
License: MIT License
Copyright: 2020 EGOIST
---
Rollup-plugin-import-css: A Rollup plugin for importing CSS files
URL: https://github.com/jleeson/rollup-plugin-import-css
License: MIT License
Copyright: 2020 Jacob Leeson
---
rxjs: Reactive Extensions for JavaScript
URL: https://rxjs.dev/
License: Apache License, Version 2.0
Copyright: 2015-present Ben Lesh <ben@benlesh.com>, Google, Inc., Netflix, Inc., Microsoft Corp., and contributors
---
Serilog: A diagnostic logging library for .NET
URL: https://github.com/serilog/serilog
License: Apache License, Version 2.0
Copyright: Serilog Contributors
---
Simple Icons: A set of SVG icons for popular brands
URL: https://simpleicons.org/
License: CC0 1.0 Universal License
Copyright: Simple Icons Contributors
---
Storybook: A UI component explorer for Web Components
URL: https://storybook.js.org/
License: MIT License
Copyright: 2024 Storybook
---
StyleCop.Analyzers: Analyzers for StyleCop
URL: https://github.com/DotNetAnalyzers/StyleCopAnalyzers
License: MIT License
Copyright: Tunnel Vision Laboratories, LLC
---
SVGO: A tool for optimizing SVG files
URL: https://svgo.dev/
License: MIT License
Copyright: Kir Belevich
---
Swashbuckle.AspNetCore: A library for generating Swagger documentation for ASP.NET Core APIs
URL: https://github.com/domaindrivendev/Swashbuckle.AspNetCore
License: MIT License
Copyright: 2016 Richard Morris
---
Tiny Glob: A tiny globbing library for Node.js
URL: https://github.com/terkelg/tiny-glob
License: MIT License
Copyright: 2018 Terkel
---
Tiptap: A renderless rich-text editor for the web
URL: https://tiptap.dev/
License: MIT License
Copyright: 2025 Tiptap GmbH
---
Tsc-alias: A TypeScript compiler plugin for aliasing module paths
URL: https://github.com/justkey007/tsc-alias
License: MIT License
Copyright: 2018 Justkey
---
Typedoc: A documentation generator for TypeScript projects
URL: https://typedoc.org/
License: Apache License, Version 2.0
Copyright: Gerrit Birkeland and Contributors
---
Typescript: A typed superset of JavaScript that compiles to plain JavaScript
URL: https://www.typescriptlang.org/
License: Apache License, Version 2.0
Copyright: 2012-present Microsoft Corporation
---
Typescript-eslint: A set of tools for linting TypeScript code
URL: https://github.com/typescript-eslint/typescript-eslint
License: MIT License
Copyright: 2019 typescript-eslint and other contributors
---
Typescript-json-schema: A library for generating JSON schema from TypeScript types
URL: https://github.com/YousefED/typescript-json-schema
License: BSD 3-Clause License
Copyright: 2016 typescript-json-schema contributors
---
Umbraco.Code: Provides code-level tools for Umbraco
URL: https://github.com/umbraco/Umbraco-Code
License: MIT License
Copyright: 2005-present Umbraco A/S
---
Umbraco.GitVersioning.Extensions: Utilities for Nerdbank.GitVersioning
URL: https://github.com/umbraco/Umbraco.GitVersioning.Extensions
License: MIT License
Copyright: 2005-present Umbraco A/S
---
Umbraco.JsonSchema.Extensions: Utilities for JSON schema generation
URL: https://github.com/umbraco/Umbraco.JsonSchema.Extensions
License: MIT License
Copyright: 2005-present Umbraco A/S
---
Umbraco UI Library: A set of UI components for building web applications
URL: https://uui.umbraco.com/
License: MIT License
Copyright: 2005-present Umbraco A/S
---
uuid: A library for generating unique identifiers
URL: https://github.com/uuidjs/uuid
License: MIT License
Copyright: 2010-2020 Robert Kieffer and other contributors
---
Vite: A fast build tool and development server for modern web projects
URL: https://vite.dev/
License: MIT License
Copyright: 2019-present VoidZero Inc. and Vite contributors
---
Vite-plugin-static-copy: A Vite plugin for copying static files
URL: https://github.com/sapphi-red/vite-plugin-static-copy
License: MIT License
Copyright: 2021 sapphi-red
---
Vite-tsconfig-paths: A Vite plugin for resolving TypeScript paths
URL: https://github.com/aleclarson/vite-tsconfig-paths
License: MIT License
Copyright: Alec Larson
---
Web Component Analyzer: A tool for analyzing web components
URL: https://github.com/runem/web-component-analyzer
License: MIT License
Copyright: 2019 Rune Mehlsen
@@ -13,7 +13,7 @@ namespace Umbraco.Cms.Api.Delivery.Controllers.Content;
[DeliveryApiAccess]
[VersionedDeliveryApiRoute("content")]
[ApiExplorerSettings(GroupName = "Content")]
[LocalizeFromAcceptLanguageHeader]
[ContextualizeFromAcceptHeaders]
[ValidateStartItem]
[AddVaryHeader]
[OutputCache(PolicyName = Constants.DeliveryApi.OutputCache.ContentCachePolicy)]
@@ -49,6 +49,7 @@ public static class UmbracoBuilderExtensions
: provider.GetRequiredService<RequestContextOutputExpansionStrategyV2>();
});
builder.Services.AddSingleton<IRequestCultureService, RequestCultureService>();
builder.Services.AddSingleton<IRequestSegmmentService, RequestSegmentService>();
builder.Services.AddSingleton<IRequestRoutingService, RequestRoutingService>();
builder.Services.AddSingleton<IRequestRedirectService, RequestRedirectService>();
builder.Services.AddSingleton<IRequestPreviewService, RequestPreviewService>();
@@ -0,0 +1,53 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
using Umbraco.Cms.Core.DeliveryApi;
using Umbraco.Cms.Core.Models.PublishedContent;
using Umbraco.Extensions;
namespace Umbraco.Cms.Api.Delivery.Filters;
internal sealed class ContextualizeFromAcceptHeadersAttribute : TypeFilterAttribute
{
public ContextualizeFromAcceptHeadersAttribute()
: base(typeof(LocalizeFromAcceptLanguageHeaderAttributeFilter))
{
}
private class LocalizeFromAcceptLanguageHeaderAttributeFilter : IActionFilter
{
private readonly IRequestCultureService _requestCultureService;
private readonly IRequestSegmmentService _requestSegmentService;
private readonly IVariationContextAccessor _variationContextAccessor;
public LocalizeFromAcceptLanguageHeaderAttributeFilter(
IRequestCultureService requestCultureService,
IRequestSegmmentService requestSegmentService,
IVariationContextAccessor variationContextAccessor)
{
_requestCultureService = requestCultureService;
_requestSegmentService = requestSegmentService;
_variationContextAccessor = variationContextAccessor;
}
public void OnActionExecuting(ActionExecutingContext context)
{
var requestedCulture = _requestCultureService.GetRequestedCulture().NullOrWhiteSpaceAsNull();
var requestedSegment = _requestSegmentService.GetRequestedSegment().NullOrWhiteSpaceAsNull();
if (requestedCulture.IsNullOrWhiteSpace() && requestedSegment.IsNullOrWhiteSpace())
{
return;
}
// contextualize the request
// NOTE: request culture or segment can be null here (but not both), so make sure to retain any existing
// context by means of fallback to current variation context (if available)
_variationContextAccessor.VariationContext = new VariationContext(
requestedCulture ?? _variationContextAccessor.VariationContext?.Culture,
requestedSegment ?? _variationContextAccessor.VariationContext?.Segment);
}
public void OnActionExecuted(ActionExecutedContext context)
{
}
}
}
@@ -1,37 +0,0 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
using Umbraco.Cms.Core.DeliveryApi;
using Umbraco.Extensions;
namespace Umbraco.Cms.Api.Delivery.Filters;
internal sealed class LocalizeFromAcceptLanguageHeaderAttribute : TypeFilterAttribute
{
public LocalizeFromAcceptLanguageHeaderAttribute()
: base(typeof(LocalizeFromAcceptLanguageHeaderAttributeFilter))
{
}
private class LocalizeFromAcceptLanguageHeaderAttributeFilter : IActionFilter
{
private readonly IRequestCultureService _requestCultureService;
public LocalizeFromAcceptLanguageHeaderAttributeFilter(IRequestCultureService requestCultureService)
=> _requestCultureService = requestCultureService;
public void OnActionExecuting(ActionExecutingContext context)
{
var requestedCulture = _requestCultureService.GetRequestedCulture();
if (requestedCulture.IsNullOrWhiteSpace())
{
return;
}
_requestCultureService.SetRequestCulture(requestedCulture);
}
public void OnActionExecuted(ActionExecutedContext context)
{
}
}
}
@@ -33,6 +33,20 @@ internal sealed class SwaggerContentDocumentationFilter : SwaggerDocumentationFi
}
});
operation.Parameters.Add(new OpenApiParameter
{
Name = "Accept-Segment",
In = ParameterLocation.Header,
Required = false,
Description = "Defines the segment to return. Use this when querying segment variant content items.",
Schema = new OpenApiSchema { Type = "string" },
Examples = new Dictionary<string, OpenApiExample>
{
{ "Default", new OpenApiExample { Value = new OpenApiString(string.Empty) } },
{ "Segment One", new OpenApiExample { Value = new OpenApiString("segment-one") } }
}
});
AddApiKey(operation);
operation.Parameters.Add(new OpenApiParameter
@@ -2,17 +2,15 @@
using Microsoft.AspNetCore.Http;
using Microsoft.Net.Http.Headers;
using Umbraco.Cms.Core.DeliveryApi;
using Umbraco.Cms.Core.Models.PublishedContent;
namespace Umbraco.Cms.Api.Delivery.Services;
internal sealed partial class RequestCultureService : RequestHeaderHandler, IRequestCultureService
{
private readonly IVariationContextAccessor _variationContextAccessor;
public RequestCultureService(IHttpContextAccessor httpContextAccessor, IVariationContextAccessor variationContextAccessor)
: base(httpContextAccessor) =>
_variationContextAccessor = variationContextAccessor;
public RequestCultureService(IHttpContextAccessor httpContextAccessor)
: base(httpContextAccessor)
{
}
/// <inheritdoc />
public string? GetRequestedCulture()
@@ -21,15 +19,10 @@ internal sealed partial class RequestCultureService : RequestHeaderHandler, IReq
return ValidLanguageHeaderRegex().IsMatch(acceptLanguage) ? acceptLanguage : null;
}
/// <inheritdoc />
[Obsolete("Use IVariationContextAccessor to manipulate the variation context. Scheduled for removal in V17.")]
public void SetRequestCulture(string culture)
{
if (_variationContextAccessor.VariationContext?.Culture == culture)
{
return;
}
_variationContextAccessor.VariationContext = new VariationContext(culture);
// no-op
}
// at the time of writing we're introducing this to get rid of accept-language header values like "en-GB,en-US;q=0.9,en;q=0.8",
@@ -1,6 +1,5 @@
using Microsoft.AspNetCore.Http;
using Umbraco.Cms.Core.DeliveryApi;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.PublishedContent;
using Umbraco.Cms.Core.PublishedCache;
using Umbraco.Cms.Core.Routing;
@@ -11,14 +10,19 @@ namespace Umbraco.Cms.Api.Delivery.Services;
internal sealed class RequestRoutingService : RoutingServiceBase, IRequestRoutingService
{
private readonly IRequestCultureService _requestCultureService;
private readonly IVariationContextAccessor _variationContextAccessor;
public RequestRoutingService(
IDomainCache domainCache,
IHttpContextAccessor httpContextAccessor,
IRequestStartItemProviderAccessor requestStartItemProviderAccessor,
IRequestCultureService requestCultureService)
: base(domainCache, httpContextAccessor, requestStartItemProviderAccessor) =>
IRequestCultureService requestCultureService,
IVariationContextAccessor variationContextAccessor)
: base(domainCache, httpContextAccessor, requestStartItemProviderAccessor)
{
_requestCultureService = requestCultureService;
_variationContextAccessor = variationContextAccessor;
}
/// <inheritdoc />
public string GetContentRoute(string requestedPath)
@@ -50,9 +54,14 @@ internal sealed class RequestRoutingService : RoutingServiceBase, IRequestRoutin
}
// the Accept-Language header takes precedence over configured domain culture
if (domainAndUri.Culture != null && _requestCultureService.GetRequestedCulture().IsNullOrWhiteSpace())
if (domainAndUri.Culture != null
&& _requestCultureService.GetRequestedCulture().IsNullOrWhiteSpace()
&& _variationContextAccessor.VariationContext?.Culture != domainAndUri.Culture)
{
_requestCultureService.SetRequestCulture(domainAndUri.Culture);
// update the variation context to match the requested domain culture while retaining any contextualized segment
_variationContextAccessor.VariationContext = new VariationContext(
culture: domainAndUri.Culture,
segment: _variationContextAccessor.VariationContext?.Segment);
}
// when resolving content from a configured domain, the content cache expects the content route
@@ -0,0 +1,16 @@
using Microsoft.AspNetCore.Http;
using Umbraco.Cms.Core.DeliveryApi;
namespace Umbraco.Cms.Api.Delivery.Services;
internal sealed class RequestSegmentService : RequestHeaderHandler, IRequestSegmmentService
{
public RequestSegmentService(IHttpContextAccessor httpContextAccessor)
: base(httpContextAccessor)
{
}
/// <inheritdoc />
public string? GetRequestedSegment()
=> GetHeaderValue("Accept-Segment");
}
@@ -27,6 +27,10 @@ public abstract class ContentControllerBase : ManagementApiControllerBase
.WithTitle("Content type culture variance mismatch")
.WithDetail("The content type variance did not match that of the passed content data.")
.Build()),
ContentEditingOperationStatus.ContentTypeSegmentVarianceMismatch => BadRequest(problemDetailsBuilder
.WithTitle("Content type segment variance mismatch")
.WithDetail("The content type variance did not match that of the passed content data.")
.Build()),
ContentEditingOperationStatus.NotFound => NotFound(problemDetailsBuilder
.WithTitle("The content could not be found")
.Build()),
@@ -24,7 +24,7 @@ public class SearchDocumentItemController : DocumentItemControllerBase
[HttpGet("search")]
[MapToApiVersion("1.0")]
[ProducesResponseType(typeof(PagedModel<DocumentItemResponseModel>), StatusCodes.Status200OK)]
public Task<IActionResult> SearchWithTrashed(
public async Task<IActionResult> SearchWithTrashed(
CancellationToken cancellationToken,
string query,
bool? trashed = null,
@@ -33,13 +33,13 @@ public class SearchDocumentItemController : DocumentItemControllerBase
Guid? parentId = null,
[FromQuery] IEnumerable<Guid>? allowedDocumentTypes = null)
{
PagedModel<IEntitySlim> searchResult = _indexedEntitySearchService.Search(UmbracoObjectTypes.Document, query, parentId, allowedDocumentTypes, trashed, skip, take);
PagedModel<IEntitySlim> searchResult = await _indexedEntitySearchService.SearchAsync(UmbracoObjectTypes.Document, query, parentId, allowedDocumentTypes, trashed, skip, take);
var result = new PagedModel<DocumentItemResponseModel>
{
Items = searchResult.Items.OfType<IDocumentEntitySlim>().Select(_documentPresentationFactory.CreateItemResponseModel),
Total = searchResult.Total,
};
return Task.FromResult<IActionResult>(Ok(result));
return Ok(result);
}
}
@@ -0,0 +1,37 @@
using Asp.Versioning;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Api.Management.ViewModels.DocumentBlueprint;
using Umbraco.Cms.Core.Mapping;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Web.Common.Authorization;
namespace Umbraco.Cms.Api.Management.Controllers.DocumentBlueprint;
[ApiVersion("1.0")]
[Authorize(Policy = AuthorizationPolicies.TreeAccessDocuments)]
public class ScaffoldDocumentBlueprintController : DocumentBlueprintControllerBase
{
private readonly IContentBlueprintEditingService _contentBlueprintEditingService;
private readonly IUmbracoMapper _umbracoMapper;
public ScaffoldDocumentBlueprintController(IContentBlueprintEditingService contentBlueprintEditingService, IUmbracoMapper umbracoMapper)
{
_contentBlueprintEditingService = contentBlueprintEditingService;
_umbracoMapper = umbracoMapper;
}
[HttpGet("{id:guid}/scaffold")]
[MapToApiVersion("1.0")]
[ProducesResponseType(typeof(DocumentBlueprintResponseModel), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
public async Task<IActionResult> Scaffold(CancellationToken cancellationToken, Guid id)
{
IContent? blueprint = await _contentBlueprintEditingService.GetScaffoldedAsync(id);
return blueprint is not null
? Ok(_umbracoMapper.Map<DocumentBlueprintResponseModel>(blueprint))
: DocumentBlueprintNotFound();
}
}
@@ -29,15 +29,15 @@ public class SearchMediaItemController : MediaItemControllerBase
[HttpGet("search")]
[MapToApiVersion("1.0")]
[ProducesResponseType(typeof(PagedModel<MediaItemResponseModel>), StatusCodes.Status200OK)]
public Task<IActionResult> SearchFromParentWithAllowedTypes(CancellationToken cancellationToken, string query, bool? trashed = null, int skip = 0, int take = 100, Guid? parentId = null, [FromQuery]IEnumerable<Guid>? allowedMediaTypes = null)
public async Task<IActionResult> SearchFromParentWithAllowedTypes(CancellationToken cancellationToken, string query, bool? trashed = null, int skip = 0, int take = 100, Guid? parentId = null, [FromQuery]IEnumerable<Guid>? allowedMediaTypes = null)
{
PagedModel<IEntitySlim> searchResult = _indexedEntitySearchService.Search(UmbracoObjectTypes.Media, query, parentId, allowedMediaTypes, trashed, skip, take);
PagedModel<IEntitySlim> searchResult = await _indexedEntitySearchService.SearchAsync(UmbracoObjectTypes.Media, query, parentId, allowedMediaTypes, trashed, skip, take);
var result = new PagedModel<MediaItemResponseModel>
{
Items = searchResult.Items.OfType<IMediaEntitySlim>().Select(_mediaPresentationFactory.CreateItemResponseModel),
Total = searchResult.Total,
};
return Task.FromResult<IActionResult>(Ok(result));
return Ok(result);
}
}
@@ -1,6 +1,4 @@
using Umbraco.Cms.Api.Management.Extensions;
using Umbraco.Cms.Api.Management.ViewModels.Content;
using Umbraco.Cms.Core.Models.ContentEditing;
using Umbraco.Cms.Core.Models.ContentEditing;
namespace Umbraco.Cms.Api.Management.Factories;
@@ -8,32 +6,22 @@ internal abstract class ContentEditingPresentationFactory<TValueModel, TVariantM
where TValueModel : ValueModelBase
where TVariantModel : VariantModelBase
{
protected TContentEditingModel MapContentEditingModel<TContentEditingModel>(ContentModelBase<TValueModel, TVariantModel> contentModel)
protected TContentEditingModel MapContentEditingModel<TContentEditingModel>(
ContentModelBase<TValueModel, TVariantModel> contentModel)
where TContentEditingModel : ContentEditingModelBase, new()
{
TVariantModel? invariantVariant = contentModel.Variants.FirstOrDefault(variant => variant.DoesNotVaryByCulture() && variant.DoesNotVaryBySegment());
TValueModel[] invariantProperties = contentModel.Values.Where(value => value.DoesNotVaryByCulture() && value.DoesNotVaryBySegment()).ToArray();
PropertyValueModel ToPropertyValueModel(TValueModel valueModel)
=> new() { Alias = valueModel.Alias, Value = valueModel.Value };
return new TContentEditingModel
=> new()
{
InvariantName = invariantVariant?.Name,
InvariantProperties = invariantProperties.Select(ToPropertyValueModel).ToArray(),
Properties = contentModel
.Values
.Select(value => new PropertyValueModel
{
Alias = value.Alias, Value = value.Value, Culture = value.Culture, Segment = value.Segment
}).ToArray(),
Variants = contentModel
.Variants
.Where(variant => variant.VariesByCulture() || variant.VariesBySegment())
.Select(variant => new VariantModel
{
Culture = variant.Culture,
Segment = variant.Segment,
Name = variant.Name,
Properties = contentModel
.Values
.Where(value => value.Culture == variant.Culture && value.Segment == variant.Segment)
.Select(ToPropertyValueModel).ToArray()
Culture = variant.Culture, Segment = variant.Segment, Name = variant.Name
})
};
}
}
@@ -1,4 +1,5 @@
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Api.Management.ViewModels;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Services;
using Umbraco.Extensions;
using ContentTypeEditingModels = Umbraco.Cms.Core.Models.ContentTypeEditing;
@@ -38,12 +39,27 @@ internal abstract class ContentTypeEditingPresentationFactory<TContentType>
VariesByCulture = viewModel.VariesByCulture,
VariesBySegment = viewModel.VariesBySegment,
Containers = MapContainers<TPropertyTypeContainerEditingModel>(viewModel.Containers),
Properties = MapProperties<TPropertyTypeEditingModel>(viewModel.Properties)
Properties = MapProperties<TPropertyTypeEditingModel>(viewModel.Properties),
};
return editingModel;
}
protected Guid? CalculateCreateContainerKey(ReferenceByIdModel? parent, IDictionary<Guid, ContentTypeViewModels.CompositionType> compositions)
{
// special case:
// the API is somewhat confusing when it comes to inheritance. the parent denotes a container (folder), but it
// is easily confused with the parent for inheritance.
// if the request model contains the same key for container and "inheritance composition", we'll be lenient and
// allow it - just remove the container, inheritance takes precedence as intent.
Guid? parentId = parent?.Id;
return parentId.HasValue
&& compositions.TryGetValue(parentId.Value, out ContentTypeViewModels.CompositionType compositionType)
&& compositionType is ContentTypeViewModels.CompositionType.Inheritance
? null
: parentId;
}
protected T MapCompositionModel<T>(ContentTypeAvailableCompositionsResult compositionResult)
where T : ContentTypeViewModels.AvailableContentTypeCompositionResponseModelBase, new()
{
@@ -45,8 +45,6 @@ internal sealed class DocumentPresentationFactory : IDocumentPresentationFactory
{
DocumentResponseModel responseModel = _umbracoMapper.Map<DocumentResponseModel>(content)!;
responseModel.Urls = await _documentUrlFactory.CreateUrlsAsync(content);
Guid? templateKey = content.TemplateId.HasValue
? _templateService.GetAsync(content.TemplateId.Value).Result?.Key
: null;
@@ -62,8 +60,6 @@ internal sealed class DocumentPresentationFactory : IDocumentPresentationFactory
{
PublishedDocumentResponseModel responseModel = _umbracoMapper.Map<PublishedDocumentResponseModel>(content)!;
responseModel.Urls = await _documentUrlFactory.CreateUrlsAsync(content);
Guid? templateKey = content.PublishTemplateId.HasValue
? _templateService.GetAsync(content.PublishTemplateId.Value).Result?.Key
: null;
@@ -80,8 +76,6 @@ internal sealed class DocumentPresentationFactory : IDocumentPresentationFactory
DocumentResponseModel responseModel = _umbracoMapper.Map<DocumentResponseModel>(content)!;
_umbracoMapper.Map(schedule, responseModel);
responseModel.Urls = await _documentUrlFactory.CreateUrlsAsync(content);
Guid? templateKey = content.TemplateId.HasValue
? _templateService.GetAsync(content.TemplateId.Value).Result?.Key
: null;
@@ -25,12 +25,14 @@ internal sealed class DocumentTypeEditingPresentationFactory : ContentTypeEditin
MapCleanup(createModel, requestModel.Cleanup);
createModel.Key = requestModel.Id;
createModel.ContainerKey = requestModel.Parent?.Id;
createModel.AllowedTemplateKeys = requestModel.AllowedTemplates.Select(reference => reference.Id).ToArray();
createModel.DefaultTemplateKey = requestModel.DefaultTemplate?.Id;
createModel.ListView = requestModel.Collection?.Id;
createModel.AllowedContentTypes = MapAllowedContentTypes(requestModel.AllowedDocumentTypes);
createModel.Compositions = MapCompositions(requestModel.Compositions);
IDictionary<Guid, ViewModels.ContentType.CompositionType> compositionTypesByKey = CompositionTypesByKey(requestModel.Compositions);
createModel.Compositions = MapCompositions(compositionTypesByKey);
createModel.ContainerKey = CalculateCreateContainerKey(requestModel.Parent, compositionTypesByKey);
return createModel;
}
@@ -51,7 +53,7 @@ internal sealed class DocumentTypeEditingPresentationFactory : ContentTypeEditin
updateModel.DefaultTemplateKey = requestModel.DefaultTemplate?.Id;
updateModel.ListView = requestModel.Collection?.Id;
updateModel.AllowedContentTypes = MapAllowedContentTypes(requestModel.AllowedDocumentTypes);
updateModel.Compositions = MapCompositions(requestModel.Compositions);
updateModel.Compositions = MapCompositions(CompositionTypesByKey(requestModel.Compositions));
return updateModel;
}
@@ -72,8 +74,8 @@ internal sealed class DocumentTypeEditingPresentationFactory : ContentTypeEditin
.DistinctBy(t => t.DocumentType.Id)
.ToDictionary(t => t.DocumentType.Id, t => t.SortOrder));
private IEnumerable<Composition> MapCompositions(IEnumerable<DocumentTypeComposition> documentTypeCompositions)
=> MapCompositions(documentTypeCompositions
private IDictionary<Guid, ViewModels.ContentType.CompositionType> CompositionTypesByKey(IEnumerable<DocumentTypeComposition> documentTypeCompositions)
=> documentTypeCompositions
.DistinctBy(c => c.DocumentType.Id)
.ToDictionary(c => c.DocumentType.Id, c => c.CompositionType));
.ToDictionary(c => c.DocumentType.Id, c => c.CompositionType);
}
@@ -26,7 +26,7 @@ internal sealed class DocumentVersionPresentationFactory : IDocumentVersionPrese
new ReferenceByIdModel(_entityService.GetKey(contentVersion.ContentTypeId, UmbracoObjectTypes.DocumentType)
.Result),
new ReferenceByIdModel(await _userIdKeyResolver.GetAsync(contentVersion.UserId)),
new DateTimeOffset(contentVersion.VersionDate, TimeSpan.Zero), // todo align with datetime offset rework
new DateTimeOffset(contentVersion.VersionDate),
contentVersion.CurrentPublishedVersion,
contentVersion.CurrentDraftVersion,
contentVersion.PreventCleanup);
@@ -23,11 +23,13 @@ internal sealed class MediaTypeEditingPresentationFactory : ContentTypeEditingPr
>(requestModel);
createModel.Key = requestModel.Id;
createModel.ContainerKey = requestModel.Parent?.Id;
createModel.AllowedContentTypes = MapAllowedContentTypes(requestModel.AllowedMediaTypes);
createModel.Compositions = MapCompositions(requestModel.Compositions);
createModel.ListView = requestModel.Collection?.Id;
IDictionary<Guid, ViewModels.ContentType.CompositionType> compositionTypesByKey = CompositionTypesByKey(requestModel.Compositions);
createModel.Compositions = MapCompositions(compositionTypesByKey);
createModel.ContainerKey = CalculateCreateContainerKey(requestModel.Parent, compositionTypesByKey);
return createModel;
}
@@ -42,7 +44,8 @@ internal sealed class MediaTypeEditingPresentationFactory : ContentTypeEditingPr
>(requestModel);
updateModel.AllowedContentTypes = MapAllowedContentTypes(requestModel.AllowedMediaTypes);
updateModel.Compositions = MapCompositions(requestModel.Compositions);
updateModel.Compositions = MapCompositions(CompositionTypesByKey(requestModel.Compositions));
updateModel.ListView = requestModel.Collection?.Id;
return updateModel;
@@ -56,8 +59,8 @@ internal sealed class MediaTypeEditingPresentationFactory : ContentTypeEditingPr
.DistinctBy(t => t.MediaType.Id)
.ToDictionary(t => t.MediaType.Id, t => t.SortOrder));
private IEnumerable<Composition> MapCompositions(IEnumerable<MediaTypeComposition> documentTypeCompositions)
=> MapCompositions(documentTypeCompositions
private IDictionary<Guid, ViewModels.ContentType.CompositionType> CompositionTypesByKey(IEnumerable<MediaTypeComposition> documentTypeCompositions)
=> documentTypeCompositions
.DistinctBy(c => c.MediaType.Id)
.ToDictionary(c => c.MediaType.Id, c => c.CompositionType));
.ToDictionary(c => c.MediaType.Id, c => c.CompositionType);
}
@@ -56,7 +56,8 @@ public abstract class ContentMapDefinition<TContent, TValueViewModel, TVariantVi
{
IPropertyValue[] propertyValues = source.Properties.SelectMany(propertyCollection => propertyCollection.Values).ToArray();
var cultures = source.AvailableCultures.DefaultIfEmpty(null).ToArray();
var segments = propertyValues.Select(property => property.Segment).Distinct().DefaultIfEmpty(null).ToArray();
// the default segment (null) must always be included in the view model - both for variant and invariant documents
var segments = propertyValues.Select(property => property.Segment).Union([null]).Distinct().ToArray();
return cultures
.SelectMany(culture => segments.Select(segment =>
@@ -83,24 +83,9 @@ public abstract class ContentTypeMapDefinition<TContentType, TPropertyTypeModel,
? CompositionType.Inheritance
: CompositionType.Composition;
protected static IEnumerable<T> MapNestedCompositions<T>(IEnumerable<IContentTypeComposition> directCompositions, int contentTypeParentId, Func<ReferenceByIdModel, CompositionType, T> contentTypeCompositionFactory)
{
var allCompositions = new List<T>();
foreach (var composition in directCompositions)
{
CompositionType compositionType = CalculateCompositionType(contentTypeParentId, composition);
T contentTypeComposition = contentTypeCompositionFactory(new ReferenceByIdModel(composition.Key), compositionType);
allCompositions.Add(contentTypeComposition);
// When we have composition inheritance, we have to find all ancestor compositions recursively
if (compositionType == CompositionType.Inheritance && composition.ContentTypeComposition.Any())
{
var nestedCompositions = MapNestedCompositions(composition.ContentTypeComposition, composition.ParentId, contentTypeCompositionFactory);
allCompositions.AddRange(nestedCompositions);
}
}
return allCompositions;
}
protected static IEnumerable<T> MapCompositions<T>(IEnumerable<IContentTypeComposition> directCompositions, int contentTypeParentId, Func<ReferenceByIdModel, CompositionType, T> contentTypeCompositionFactory)
=> directCompositions
.Select(composition => contentTypeCompositionFactory(
new ReferenceByIdModel(composition.Key),
CalculateCompositionType(contentTypeParentId, composition))).ToArray();
}
@@ -41,7 +41,7 @@ public class DocumentTypeMapDefinition : ContentTypeMapDefinition<IContentType,
new DocumentTypeSort { DocumentType = new ReferenceByIdModel(ct.Key), SortOrder = ct.SortOrder })
.OrderBy(ct => ct.SortOrder)
.ToArray() ?? Enumerable.Empty<DocumentTypeSort>();
target.Compositions = MapNestedCompositions(
target.Compositions = MapCompositions(
source.ContentTypeComposition,
source.ParentId,
(referenceByIdModel, compositionType) => new DocumentTypeComposition
@@ -40,7 +40,7 @@ public class MediaTypeMapDefinition : ContentTypeMapDefinition<IMediaType, Media
target.AllowedMediaTypes = source.AllowedContentTypes?.Select(ct =>
new MediaTypeSort { MediaType = new ReferenceByIdModel(ct.Key), SortOrder = ct.SortOrder })
.ToArray() ?? Enumerable.Empty<MediaTypeSort>();
target.Compositions = MapNestedCompositions(
target.Compositions = MapCompositions(
source.ContentTypeComposition,
source.ParentId,
(referenceByIdModel, compositionType) => new MediaTypeComposition
@@ -32,7 +32,7 @@ public class MemberTypeMapDefinition : ContentTypeMapDefinition<IMemberType, Mem
target.IsElement = source.IsElement;
target.Containers = MapPropertyTypeContainers(source);
target.Properties = MapPropertyTypes(source);
target.Compositions = MapNestedCompositions(
target.Compositions = MapCompositions(
source.ContentTypeComposition,
source.ParentId,
(referenceByIdModel, compositionType) => new MemberTypeComposition
+65 -3
View File
@@ -1,5 +1,5 @@
{
"openapi": "3.0.1",
"openapi": "3.0.4",
"info": {
"title": "Umbraco Management API",
"description": "This shows all APIs available in this version of Umbraco - including all the legacy apis that are available for backward compatibility",
@@ -3485,6 +3485,66 @@
]
}
},
"/umbraco/management/api/v1/document-blueprint/{id}/scaffold": {
"get": {
"tags": [
"Document Blueprint"
],
"operationId": "GetDocumentBlueprintByIdScaffold",
"parameters": [
{
"name": "id",
"in": "path",
"required": true,
"schema": {
"type": "string",
"format": "uuid"
}
}
],
"responses": {
"200": {
"description": "OK",
"content": {
"application/json": {
"schema": {
"oneOf": [
{
"$ref": "#/components/schemas/DocumentBlueprintResponseModel"
}
]
}
}
}
},
"404": {
"description": "Not Found",
"content": {
"application/json": {
"schema": {
"oneOf": [
{
"$ref": "#/components/schemas/ProblemDetails"
}
]
}
}
}
},
"401": {
"description": "The resource is protected and requires an authentication token"
},
"403": {
"description": "The authenticated user does not have access to this resource"
}
},
"security": [
{
"Backoffice User": [ ]
}
]
}
},
"/umbraco/management/api/v1/document-blueprint/folder": {
"post": {
"tags": [
@@ -37553,7 +37613,8 @@
"$ref": "#/components/schemas/DocumentUrlInfoModel"
}
]
}
},
"deprecated": true
},
"template": {
"oneOf": [
@@ -43429,7 +43490,8 @@
"$ref": "#/components/schemas/DocumentUrlInfoModel"
}
]
}
},
"deprecated": true
},
"template": {
"oneOf": [
@@ -33,8 +33,11 @@ public class ConfigureUmbracoBackofficeJsonOptions : IConfigureNamedOptions<Json
options.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter());
options.JsonSerializerOptions.Converters.Add(new JsonUdiConverter());
options.JsonSerializerOptions.Converters.Add(new JsonUdiRangeConverter());
options.JsonSerializerOptions.Converters.Add(new ValidationProblemDetailsConverter());
options.JsonSerializerOptions.Converters.Add(new JsonObjectConverter());
options.JsonSerializerOptions.TypeInfoResolver = _umbracoJsonTypeInfoResolver;
options.JsonSerializerOptions.MaxDepth = 64; // Ensures the maximum possible value is used, in particular to support handling as best we can levels of nested blocks.
}
}
@@ -0,0 +1,70 @@
using System.Text.Json;
using System.Text.Json.Serialization;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Core;
using Umbraco.Extensions;
namespace Umbraco.Cms.Api.Management.Serialization;
/// <summary>
/// Custom JSON converter registered via <see cref="ConfigureUmbracoBackofficeJsonOptions"/> to serialize <see cref="ValidationProblemDetails" /> to JSON.
/// </summary>
public sealed class ValidationProblemDetailsConverter : JsonConverter<ValidationProblemDetails>
{
/// <inheritdoc />
public override ValidationProblemDetails? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
=> null;
/// <inheritdoc />
public override void Write(Utf8JsonWriter writer, ValidationProblemDetails value, JsonSerializerOptions options)
{
writer.WriteStartObject();
writer.WriteString("type", value.Type);
writer.WriteString("title", value.Title);
if (value.Status.HasValue)
{
writer.WriteNumber("status", value.Status.Value);
}
if (value.Detail is not null)
{
writer.WriteString("detail", value.Detail);
}
if (value.Instance is not null)
{
writer.WriteString("instance", value.Instance);
}
if (value.Errors.Any())
{
writer.WritePropertyName("errors");
writer.WriteStartArray();
foreach (KeyValuePair<string, string[]> error in value.Errors)
{
writer.WriteStartObject();
writer.WritePropertyName($"$.{string.Join(".", error.Key.Split(Constants.CharArrays.Period).Select(s => s.ToFirstLowerInvariant()))}");
writer.WriteStartArray();
foreach (var errorDetails in error.Value)
{
writer.WriteStringValue(errorDetails);
}
writer.WriteEndArray();
writer.WriteEndObject();
}
writer.WriteEndArray();
}
if (value.Extensions.TryGetValue("traceId", out var traceId) && traceId is string traceIdValue)
{
writer.WriteString("traceId", traceIdValue);
}
writer.WriteEndObject();
}
}
@@ -1,7 +1,8 @@
namespace Umbraco.Cms.Api.Management.ViewModels.Document;
namespace Umbraco.Cms.Api.Management.ViewModels.Document;
public class DocumentResponseModel : DocumentResponseModelBase<DocumentValueResponseModel, DocumentVariantResponseModel>
{
[Obsolete("This property is no longer populated. Please use /document/{id}/urls instead to retrieve the URLs for a document. Scheduled for removal in Umbraco 17.")]
public IEnumerable<DocumentUrlInfo> Urls { get; set; } = Enumerable.Empty<DocumentUrlInfo>();
public ReferenceByIdModel? Template { get; set; }
@@ -1,3 +1,5 @@
using Microsoft.Extensions.DependencyInjection;
using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Core.Events;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Notifications;
@@ -21,9 +23,11 @@ public sealed class ContentCacheRefresher : PayloadCacheRefresherBase<ContentCac
private readonly IDocumentNavigationManagementService _documentNavigationManagementService;
private readonly IContentService _contentService;
private readonly IDocumentCacheService _documentCacheService;
private readonly ICacheManager _cacheManager;
private readonly IPublishStatusManagementService _publishStatusManagementService;
private readonly IIdKeyMap _idKeyMap;
[Obsolete("Use the constructor with ICacheManager instead, scheduled for removal in V17.")]
public ContentCacheRefresher(
AppCaches appCaches,
IJsonSerializer serializer,
@@ -38,6 +42,39 @@ public sealed class ContentCacheRefresher : PayloadCacheRefresherBase<ContentCac
IContentService contentService,
IPublishStatusManagementService publishStatusManagementService,
IDocumentCacheService documentCacheService)
: this(
appCaches,
serializer,
idKeyMap,
domainService,
eventAggregator,
factory,
documentUrlService,
domainCacheService,
documentNavigationQueryService,
documentNavigationManagementService,
contentService,
publishStatusManagementService,
documentCacheService,
StaticServiceProvider.Instance.GetRequiredService<ICacheManager>())
{
}
public ContentCacheRefresher(
AppCaches appCaches,
IJsonSerializer serializer,
IIdKeyMap idKeyMap,
IDomainService domainService,
IEventAggregator eventAggregator,
ICacheRefresherNotificationFactory factory,
IDocumentUrlService documentUrlService,
IDomainCacheService domainCacheService,
IDocumentNavigationQueryService documentNavigationQueryService,
IDocumentNavigationManagementService documentNavigationManagementService,
IContentService contentService,
IPublishStatusManagementService publishStatusManagementService,
IDocumentCacheService documentCacheService,
ICacheManager cacheManager)
: base(appCaches, serializer, eventAggregator, factory)
{
_idKeyMap = idKeyMap;
@@ -49,6 +86,11 @@ public sealed class ContentCacheRefresher : PayloadCacheRefresherBase<ContentCac
_contentService = contentService;
_documentCacheService = documentCacheService;
_publishStatusManagementService = publishStatusManagementService;
// TODO: Ideally we should inject IElementsCache
// this interface is in infrastructure, and changing this is very breaking
// so as long as we have the cache manager, which casts the IElementsCache to a simple AppCache we might as well use that.
_cacheManager = cacheManager;
}
#region Indirect
@@ -83,6 +125,13 @@ public sealed class ContentCacheRefresher : PayloadCacheRefresherBase<ContentCac
AppCaches.RuntimeCache.ClearOfType<PublicAccessEntry>();
AppCaches.RuntimeCache.ClearByKey(CacheKeys.ContentRecycleBinCacheKey);
// Ideally, we'd like to not have to clear the entire cache here. However, this was the existing behavior in NuCache.
// The reason for this is that we have no way to know which elements are affected by the changes or what their keys are.
// This is because currently published elements live exclusively in a JSON blob in the umbracoPropertyData table.
// This means that the only way to resolve these keys is to actually parse this data with a specific value converter, and for all cultures, which is not possible.
// If published elements become their own entities with relations, instead of just property data, we can revisit this.
_cacheManager.ElementsCache.Clear();
var idsRemoved = new HashSet<int>();
IAppPolicyCache isolatedCache = AppCaches.IsolatedCaches.GetOrCreate<IContent>();
@@ -1,3 +1,5 @@
using Microsoft.Extensions.DependencyInjection;
using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Core.Events;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Notifications;
@@ -18,7 +20,9 @@ public sealed class MediaCacheRefresher : PayloadCacheRefresherBase<MediaCacheRe
private readonly IMediaNavigationManagementService _mediaNavigationManagementService;
private readonly IMediaService _mediaService;
private readonly IMediaCacheService _mediaCacheService;
private readonly ICacheManager _cacheManager;
[Obsolete("Use the constructor with ICacheManager instead, scheduled for removal in V17.")]
public MediaCacheRefresher(
AppCaches appCaches,
IJsonSerializer serializer,
@@ -29,6 +33,31 @@ public sealed class MediaCacheRefresher : PayloadCacheRefresherBase<MediaCacheRe
IMediaNavigationManagementService mediaNavigationManagementService,
IMediaService mediaService,
IMediaCacheService mediaCacheService)
: this(
appCaches,
serializer,
idKeyMap,
eventAggregator,
factory,
mediaNavigationQueryService,
mediaNavigationManagementService,
mediaService,
mediaCacheService,
StaticServiceProvider.Instance.GetRequiredService<ICacheManager>())
{
}
public MediaCacheRefresher(
AppCaches appCaches,
IJsonSerializer serializer,
IIdKeyMap idKeyMap,
IEventAggregator eventAggregator,
ICacheRefresherNotificationFactory factory,
IMediaNavigationQueryService mediaNavigationQueryService,
IMediaNavigationManagementService mediaNavigationManagementService,
IMediaService mediaService,
IMediaCacheService mediaCacheService,
ICacheManager cacheManager)
: base(appCaches, serializer, eventAggregator, factory)
{
_idKeyMap = idKeyMap;
@@ -36,6 +65,9 @@ public sealed class MediaCacheRefresher : PayloadCacheRefresherBase<MediaCacheRe
_mediaNavigationManagementService = mediaNavigationManagementService;
_mediaService = mediaService;
_mediaCacheService = mediaCacheService;
// TODO: Use IElementsCache instead of ICacheManager, see ContentCacheRefresher for more information.
_cacheManager = cacheManager;
}
#region Indirect
@@ -87,6 +119,13 @@ public sealed class MediaCacheRefresher : PayloadCacheRefresherBase<MediaCacheRe
AppCaches.RuntimeCache.ClearByKey(CacheKeys.MediaRecycleBinCacheKey);
Attempt<IAppPolicyCache?> mediaCache = AppCaches.IsolatedCaches.Get<IMedia>();
// Ideally, we'd like to not have to clear the entire cache here. However, this was the existing behavior in NuCache.
// The reason for this is that we have no way to know which elements are affected by the changes or what their keys are.
// This is because currently published elements live exclusively in a JSON blob in the umbracoPropertyData table.
// This means that the only way to resolve these keys is to actually parse this data with a specific value converter, and for all cultures, which is not possible.
// If published elements become their own entities with relations, instead of just property data, we can revisit this.
_cacheManager.ElementsCache.Clear();
foreach (JsonPayload payload in payloads)
{
if (payload.ChangeTypes == TreeChangeTypes.Remove)
@@ -2,6 +2,7 @@
// See LICENSE for more details.
using System.ComponentModel;
using Umbraco.Cms.Core.Logging;
namespace Umbraco.Cms.Core.Configuration.Models;
@@ -13,6 +14,8 @@ public class LoggingSettings
{
internal const string StaticMaxLogAge = "1.00:00:00"; // TimeSpan.FromHours(24);
internal const string StaticDirectory = Constants.SystemDirectories.LogFiles;
internal const string StaticFileNameFormat = LoggingConfiguration.DefaultLogFileNameFormat;
internal const string StaticFileNameFormatArguments = "MachineName";
/// <summary>
/// Gets or sets a value for the maximum age of a log file.
@@ -31,4 +34,25 @@ public class LoggingSettings
/// </value>
[DefaultValue(StaticDirectory)]
public string Directory { get; set; } = StaticDirectory;
/// <summary>
/// Gets or sets the file name format to use for log files.
/// </summary>
/// <value>
/// The file name format.
/// </value>
[DefaultValue(StaticFileNameFormat)]
public string FileNameFormat { get; set; } = StaticFileNameFormat;
/// <summary>
/// Gets or sets the file name format arguments to use for log files.
/// </summary>
/// <value>
/// The file name format arguments as a comma delimited string of accepted values.
/// </value>
/// <remarks>
/// Accepted values for format arguments are: MachineName, EnvironmentName.
/// </remarks>
[DefaultValue(StaticFileNameFormatArguments)]
public string FileNameFormatArguments { get; set; } = StaticFileNameFormatArguments;
}
@@ -19,7 +19,6 @@ public class RuntimeSettings
public RuntimeMode Mode { get; set; } = RuntimeMode.BackofficeDevelopment;
/// <summary>
[Obsolete("No longer used and will be removed in Umbraco 16.")]
/// Gets or sets a value for the maximum request length in kb.
/// </summary>
public long? MaxRequestLength { get; set; }
@@ -0,0 +1,51 @@
// Copyright (c) Umbraco.
// See LICENSE for more details.
using Microsoft.Extensions.Options;
using Umbraco.Cms.Core.Logging;
namespace Umbraco.Cms.Core.Configuration.Models.Validation;
/// <summary>
/// Validator for configuration representated as <see cref="LoggingSettings" />.
/// </summary>
public class LoggingSettingsValidator : ConfigurationValidatorBase, IValidateOptions<LoggingSettings>
{
/// <inheritdoc />
public ValidateOptionsResult Validate(string? name, LoggingSettings options)
{
if (!ValidateFileNameFormatArgument(options.FileNameFormat, options.FileNameFormatArguments, out var message))
{
return ValidateOptionsResult.Fail(message);
}
return ValidateOptionsResult.Success;
}
private bool ValidateFileNameFormatArgument(string fileNameFormat, string fileNameFormatArguments, out string message)
{
var fileNameFormatArgumentsAsArray = fileNameFormatArguments
.Split([','], StringSplitOptions.RemoveEmptyEntries)
.Select(x => x.Trim())
.ToArray();
if (fileNameFormatArgumentsAsArray.Any(x => LoggingConfiguration.SupportedFileNameFormatArguments.Contains(x) is false))
{
message = $"The file name arguments '{string.Join(",", fileNameFormatArgumentsAsArray)}' contain one or more values that aren't in the supported list of values '{string.Join(",", LoggingConfiguration.SupportedFileNameFormatArguments)}'.";
return false;
}
try
{
_ = string.Format(fileNameFormat, fileNameFormatArgumentsAsArray);
}
catch (FormatException)
{
message = $"The provided file name format '{fileNameFormat}' could not be used with the provided arguments '{string.Join(",", fileNameFormatArgumentsAsArray)}'.";
return false;
}
message = string.Empty;
return true;
}
}
+13
View File
@@ -0,0 +1,13 @@
namespace Umbraco.Cms.Core;
public static partial class Constants
{
public static class Cache
{
public static class Tags
{
public const string Content = "content";
public const string Media = "media";
}
}
}
@@ -7,9 +7,6 @@ public interface IRequestCultureService
/// </summary>
string? GetRequestedCulture();
/// <summary>
/// Updates the current request culture if applicable.
/// </summary>
/// <param name="culture">The culture to use for the current request.</param>
[Obsolete("Use IVariationContextAccessor to manipulate the variation context. Scheduled for removal in V17.")]
void SetRequestCulture(string culture);
}
@@ -0,0 +1,9 @@
namespace Umbraco.Cms.Core.DeliveryApi;
public interface IRequestSegmmentService
{
/// <summary>
/// Gets the requested segment from the "Accept-Segment" header, if present.
/// </summary>
string? GetRequestedSegment();
}
@@ -43,6 +43,7 @@ public static partial class UmbracoBuilderExtensions
builder.Services.AddSingleton<IValidateOptions<ContentSettings>, ContentSettingsValidator>();
builder.Services.AddSingleton<IValidateOptions<GlobalSettings>, GlobalSettingsValidator>();
builder.Services.AddSingleton<IValidateOptions<HealthChecksSettings>, HealthChecksSettingsValidator>();
builder.Services.AddSingleton<IValidateOptions<LoggingSettings>, LoggingSettingsValidator>();
builder.Services.AddSingleton<IValidateOptions<RequestHandlerSettings>, RequestHandlerSettingsValidator>();
builder.Services.AddSingleton<IValidateOptions<UnattendedSettings>, UnattendedSettingsValidator>();
builder.Services.AddSingleton<IValidateOptions<SecuritySettings>, SecuritySettingsValidator>();
+37 -11
View File
@@ -11,17 +11,18 @@ public interface IFileType
/// <value>
/// <c>true</c> if the file can be set using a physical path; otherwise, <c>false</c>.
/// </value>
[Obsolete("An interface should not expose implementation details. Scheduled for removal in Umbraco 18.")]
bool CanSetPhysical { get; }
/// <summary>
/// Gets the stream as an asynchronous operation.
/// Gets the stream in an asynchronous operation.
/// </summary>
/// <param name="udi">The UDI.</param>
/// <param name="token">The cancellation token.</param>
/// <returns>
/// The task object representing the asynchronous operation.
/// The task object representing the asynchronous operation. The task result contains the stream.
/// </returns>
Task<Stream> GetStreamAsync(StringUdi udi, CancellationToken token);
Task<Stream> GetStreamAsync(StringUdi udi, CancellationToken token); // TODO: Rename token to cancellationToken and add default value
/// <summary>
/// Gets the checksum stream.
@@ -30,8 +31,22 @@ public interface IFileType
/// <returns>
/// The checksum stream.
/// </returns>
[Obsolete("Use GetChecksumStreamAsync() instead. Scheduled for removal in Umbraco 18.")]
Stream GetChecksumStream(StringUdi udi);
/// <summary>
/// Gets the checksum stream in an asynchronous operation.
/// </summary>
/// <param name="udi">The UDI.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>
/// The task object representing the asynchronous operation. The task result contains the checksum stream.
/// </returns>
Task<Stream> GetChecksumStreamAsync(StringUdi udi, CancellationToken cancellationToken = default)
#pragma warning disable CS0618 // Type or member is obsolete
=> Task.FromResult(GetChecksumStream(udi));
#pragma warning restore CS0618 // Type or member is obsolete
/// <summary>
/// Gets the file length in bytes or <c>-1</c> if not found.
/// </summary>
@@ -39,8 +54,22 @@ public interface IFileType
/// <returns>
/// The file length in bytes or <c>-1</c> if not found.
/// </returns>
[Obsolete("Use GetLengthAsync() instead. Scheduled for removal in Umbraco 18.")]
long GetLength(StringUdi udi);
/// <summary>
/// Gets the file length in bytes or <c>-1</c> if not found in an asynchronous operation.
/// </summary>
/// <param name="udi">The UDI.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>
/// The task object representing the asynchronous operation. The task result contains the file length in bytes or <c>-1</c> if not found.
/// </returns>
Task<long> GetLengthAsync(StringUdi udi, CancellationToken cancellationToken = default)
#pragma warning disable CS0618 // Type or member is obsolete
=> Task.FromResult(GetLength(udi));
#pragma warning restore CS0618 // Type or member is obsolete
/// <summary>
/// Sets the stream as an asynchronous operation.
/// </summary>
@@ -50,7 +79,7 @@ public interface IFileType
/// <returns>
/// The task object representing the asynchronous operation.
/// </returns>
Task SetStreamAsync(StringUdi udi, Stream stream, CancellationToken token);
Task SetStreamAsync(StringUdi udi, Stream stream, CancellationToken token); // TODO: Rename token to cancellationToken and add default value
/// <summary>
/// Sets the physical path of the file.
@@ -58,20 +87,16 @@ public interface IFileType
/// <param name="udi">The UDI.</param>
/// <param name="physicalPath">The physical path.</param>
/// <param name="copy">If set to <c>true</c> copies the file instead of moving.</param>
[Obsolete("Use SetStreamAsync() instead to not rely on physical file paths. Scheduled for removal in Umbraco 18.")]
void Set(StringUdi udi, string physicalPath, bool copy = false);
/// <summary>
/// Gets the physical path or <see cref="string.Empty"/> if not found.
/// Gets the path to the file, including the file name. Returns <see cref="string.Empty" /> if the file is not directly accessible.
/// </summary>
/// <param name="udi">The UDI.</param>
/// <returns>
/// The physical path or <see cref="string.Empty"/> if not found.
/// The path to the file, including the file name or <see cref="string.Empty" /> if the file is not directly accessible.
/// </returns>
/// <remarks>
/// This is not pretty as *everywhere* in Deploy we take care of ignoring
/// the physical path and always rely on the virtual IFileSystem,
/// but Cloud wants to add some of these files to Git and needs the path...
/// </remarks>
string GetPhysicalPath(StringUdi udi);
/// <summary>
@@ -81,5 +106,6 @@ public interface IFileType
/// <returns>
/// The virtual path or <see cref="string.Empty"/> if not found.
/// </returns>
[Obsolete("This is not used anymore. Scheduled for removal in Umbraco 18.")]
string GetVirtualPath(StringUdi udi);
}
@@ -377,7 +377,7 @@
<key alias="invalidEmpty">Value cannot be empty</key>
<key alias="invalidPattern">Value is invalid, it does not match the correct pattern</key>
<key alias="entriesShort"><![CDATA[Minimum %0% entries, requires <strong>%1%</strong> more.]]></key>
<key alias="entriesExceed"><![CDATA[Maximum %0% entries, <strong>%1%</strong> too many.]]></key>
<key alias="entriesExceed"><![CDATA[Maximum %0% entries, you have entered <strong>%1%</strong> too many.]]></key>
<key alias="stringLengthExceeded">The string length exceeds the maximum length of %0% characters, %1% too many.</key>
<key alias="entriesAreasMismatch">The content amount requirements are not met for one or more areas.</key>
<key alias="invalidMemberGroupName">Invalid member group name</key>
@@ -394,7 +394,7 @@
<key alias="unexpectedRange">The value %0% is not expected to contain a range</key>
<key alias="invalidRange">The value %0% is not expected to have a to value less than the from value</key>
<key alias="entriesShort"><![CDATA[Minimum %0% entries, requires <strong>%1%</strong> more.]]></key>
<key alias="entriesExceed"><![CDATA[Maximum %0% entries, <strong>%1%</strong> too many.]]></key>
<key alias="entriesExceed"><![CDATA[Maximum %0% entries, you have entered <strong>%1%</strong> too many.]]></key>
<key alias="stringLengthExceeded">The string length exceeds the maximum length of %0% characters, %1% too many.</key>
<key alias="entriesAreasMismatch">The content amount requirements are not met for one or more areas.</key>
<key alias="invalidMediaType">The chosen media type is invalid.</key>
@@ -245,13 +245,13 @@ public static class ContentExtensions
}
IEnumerable<ContentSchedule> expires = contentSchedule.GetSchedule(culture, ContentScheduleAction.Expire);
if (expires != null && expires.Any(x => x.Date > DateTime.MinValue && DateTime.Now > x.Date))
if (expires != null && expires.Any(x => x.Date > DateTime.MinValue && DateTime.UtcNow > x.Date))
{
return ContentStatus.Expired;
}
IEnumerable<ContentSchedule> release = contentSchedule.GetSchedule(culture, ContentScheduleAction.Release);
if (release != null && release.Any(x => x.Date > DateTime.MinValue && x.Date > DateTime.Now))
if (release != null && release.Any(x => x.Date > DateTime.MinValue && x.Date > DateTime.UtcNow))
{
return ContentStatus.AwaitingRelease;
}
@@ -3,7 +3,17 @@ namespace Umbraco.Cms.Core.Logging;
public interface ILoggingConfiguration
{
/// <summary>
/// Gets the physical path where logs are stored
/// Gets the physical path where logs are stored.
/// </summary>
string LogDirectory { get; }
/// <summary>
/// Gets the file name format for the log files.
/// </summary>
string LogFileNameFormat { get; }
/// <summary>
/// Gets the file name format arguments for the log files.
/// </summary>
string[] GetLogFileNameFormatArguments();
}
@@ -1,9 +1,73 @@
namespace Umbraco.Cms.Core.Logging;
/// <summary>
/// Implements <see cref="ILoggingConfiguration"/> to provide configuration for logging to files.
/// </summary>
public class LoggingConfiguration : ILoggingConfiguration
{
public LoggingConfiguration(string logDirectory) =>
LogDirectory = logDirectory ?? throw new ArgumentNullException(nameof(logDirectory));
/// <summary>
/// The default log file name format.
/// </summary>
public const string DefaultLogFileNameFormat = "UmbracoTraceLog.{0}..json";
/// <summary>
/// The default log file name format arguments.
/// </summary>
public const string DefaultLogFileNameFormatArguments = MachineNameFileFormatArgument;
/// <summary>
/// The collection of supported file name format arguments.
/// </summary>
public static readonly string[] SupportedFileNameFormatArguments =
{
MachineNameFileFormatArgument,
EnvironmentNameFileFormatArgument,
};
private readonly string _logFileNameFormatArguments;
private const string MachineNameFileFormatArgument = "MachineName";
private const string EnvironmentNameFileFormatArgument = "EnvironmentName";
/// <summary>
/// Initializes a new instance of the <see cref="LoggingConfiguration"/> class with the default log file name format and arguments.
/// </summary>
/// <param name="logDirectory">The log file directory.</param>
public LoggingConfiguration(string logDirectory)
: this(logDirectory, DefaultLogFileNameFormat, DefaultLogFileNameFormatArguments)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="LoggingConfiguration"/> class.
/// </summary>
/// <param name="logDirectory">The log file directory.</param>
/// <param name="logFileNameFormat">The log file name format.</param>
/// <param name="logFileNameFormatArguments">The log file name format arguments as a comma delimited string.</param>
public LoggingConfiguration(string logDirectory, string logFileNameFormat, string logFileNameFormatArguments)
{
LogDirectory = logDirectory;
LogFileNameFormat = logFileNameFormat;
_logFileNameFormatArguments = logFileNameFormatArguments;
}
/// <inheritdoc/>
public string LogDirectory { get; }
/// <inheritdoc/>
public string LogFileNameFormat { get; }
/// <inheritdoc/>
public string[] GetLogFileNameFormatArguments() => _logFileNameFormatArguments.Split(',', StringSplitOptions.RemoveEmptyEntries)
.Select(x => x.Trim())
.Select(GetValue)
.ToArray();
private static string GetValue(string arg) =>
arg switch
{
MachineNameFileFormatArgument => Environment.MachineName,
EnvironmentNameFileFormatArgument => Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") ?? "Production",
_ => string.Empty,
};
}
@@ -2,9 +2,7 @@
public abstract class ContentEditingModelBase
{
public string? InvariantName { get; set; }
public IEnumerable<PropertyValueModel> InvariantProperties { get; set; } = Array.Empty<PropertyValueModel>();
public IEnumerable<PropertyValueModel> Properties { get; set; } = Array.Empty<PropertyValueModel>();
public IEnumerable<VariantModel> Variants { get; set; } = Array.Empty<VariantModel>();
}
@@ -5,4 +5,8 @@ public class PropertyValueModel
public required string Alias { get; set; }
public required object? Value { get; set; }
public string? Culture { get; set; }
public string? Segment { get; set; }
}
@@ -7,6 +7,4 @@ public class VariantModel
public string? Segment { get; set; }
public required string Name { get; set; }
public required IEnumerable<PropertyValueModel> Properties { get; set; }
}
@@ -37,7 +37,7 @@ public class ContentVersionMeta
public int UserId { get; }
public DateTime VersionDate { get; }
public DateTime VersionDate { get; private set; }
public bool CurrentPublishedVersion { get; }
@@ -47,5 +47,7 @@ public class ContentVersionMeta
public string? Username { get; }
public void SpecifyVersionDateKind(DateTimeKind kind) => VersionDate = DateTime.SpecifyKind(VersionDate, kind);
public override string ToString() => $"ContentVersionMeta(versionId: {VersionId}, versionDate: {VersionDate:s}";
}
@@ -8,7 +8,7 @@ public sealed class PropertyValidationContext
public required IEnumerable<string> CulturesBeingValidated { get; init; }
public required IEnumerable<string> SegmentsBeingValidated { get; init; }
public required IEnumerable<string?> SegmentsBeingValidated { get; init; }
public static PropertyValidationContext Empty() => new()
{
@@ -1,8 +1,10 @@
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Umbraco.Cms.Core.Configuration.Models;
using Umbraco.Cms.Core.Events;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.ContentEditing;
using Umbraco.Cms.Core.Notifications;
using Umbraco.Cms.Core.PropertyEditors;
using Umbraco.Cms.Core.Scoping;
using Umbraco.Cms.Core.Services.OperationStatus;
@@ -35,6 +37,21 @@ internal sealed class ContentBlueprintEditingService
return Task.FromResult(blueprint);
}
public Task<IContent?> GetScaffoldedAsync(Guid key)
{
IContent? blueprint = ContentService.GetBlueprintById(key);
if (blueprint is null)
{
return Task.FromResult<IContent?>(null);
}
using ICoreScope scope = CoreScopeProvider.CreateCoreScope();
scope.Notifications.Publish(new ContentScaffoldedNotification(blueprint, blueprint, Constants.System.Root, new EventMessages()));
scope.Complete();
return Task.FromResult<IContent?>(blueprint);
}
public async Task<Attempt<PagedModel<IContent>?, ContentEditingOperationStatus>> GetPagedByContentTypeAsync(Guid contentTypeKey, int skip, int take)
{
IContentType? contentType = await ContentTypeService.GetAsync(contentTypeKey);
@@ -69,7 +86,7 @@ internal sealed class ContentBlueprintEditingService
IContent blueprint = result.Result.Content!;
if (ValidateUniqueName(createModel.InvariantName ?? string.Empty, blueprint) is false)
if (ValidateUniqueNames(createModel.Variants, blueprint) is false)
{
return Attempt.FailWithStatus(ContentEditingOperationStatus.DuplicateName, new ContentCreateResult());
}
@@ -116,7 +133,7 @@ internal sealed class ContentBlueprintEditingService
return Attempt.FailWithStatus(ContentEditingOperationStatus.NotFound, new ContentUpdateResult());
}
if (ValidateUniqueName(updateModel.InvariantName ?? string.Empty, blueprint) is false)
if (ValidateUniqueNames(updateModel.Variants, blueprint) is false)
{
return Attempt.FailWithStatus(ContentEditingOperationStatus.DuplicateName, new ContentUpdateResult());
}
@@ -232,4 +249,18 @@ internal sealed class ContentBlueprintEditingService
IEnumerable<IContent> existing = ContentService.GetBlueprintsForContentTypes(content.ContentTypeId);
return existing.Any(c => c.Name == name && c.Id != content.Id) is false;
}
private bool ValidateUniqueNames(IEnumerable<VariantModel> variants, IContent content)
{
IContent[] existing = ContentService.GetBlueprintsForContentTypes(content.ContentTypeId).ToArray();
foreach (VariantModel variant in variants)
{
if (existing.Any(c => c.GetCultureName(variant.Culture) == variant.Name && c.Id != content.Id))
{
return false;
}
}
return true;
}
}
@@ -318,34 +318,36 @@ internal abstract class ContentEditingServiceBase<TContent, TContentType, TConte
return null;
}
if (contentType.VariesByCulture() == false)
if (contentType.VariesByNothing() && contentEditingModelBase.Variants.Any(v => v.Culture is null && v.Segment is null) is false)
{
if (contentEditingModelBase.InvariantName.IsNullOrWhiteSpace() || contentEditingModelBase.Variants.Any())
{
operationStatus = ContentEditingOperationStatus.ContentTypeCultureVarianceMismatch;
return null;
}
// does not vary by anything and is missing the invariant name = invalid
operationStatus = ContentEditingOperationStatus.ContentTypeCultureVarianceMismatch;
return null;
}
if (contentType.VariesByCulture() && contentEditingModelBase.Variants.Any(v => v.Culture is null))
{
// varies by culture with one or more variants not bound to a culture = invalid
operationStatus = ContentEditingOperationStatus.ContentTypeCultureVarianceMismatch;
return null;
}
if (contentType.VariesBySegment() && contentEditingModelBase.Variants.Any(v => v.Segment is null) is false)
{
// varies by segment with no default segment variants = invalid
operationStatus = ContentEditingOperationStatus.ContentTypeSegmentVarianceMismatch;
return null;
}
var propertyTypesByAlias = contentType.CompositionPropertyTypes.ToDictionary(pt => pt.Alias);
var propertyValuesAndVariance = contentEditingModelBase
.InvariantProperties
.Properties
.Select(pv => new
{
VariesByCulture = false,
VariesBySegment = false,
VariesByCulture = pv.Culture is not null,
VariesBySegment = pv.Segment is not null,
PropertyValue = pv
})
.Union(contentEditingModelBase
.Variants
.SelectMany(v => v
.Properties
.Select(vpv => new
{
VariesByCulture = true,
VariesBySegment = v.Segment.IsNullOrWhiteSpace() == false,
PropertyValue = vpv
})))
.ToArray();
// verify that all property values are defined as property types
@@ -359,7 +361,8 @@ internal abstract class ContentEditingServiceBase<TContent, TContentType, TConte
if (propertyValuesAndVariance.Any(pv =>
{
IPropertyType propertyType = propertyTypesByAlias[pv.PropertyValue.Alias];
return propertyType.VariesByCulture() != pv.VariesByCulture || propertyType.VariesBySegment() != pv.VariesBySegment;
return (propertyType.VariesByCulture() != pv.VariesByCulture)
|| (propertyType.VariesBySegment() is false && pv.VariesBySegment);
}))
{
operationStatus = ContentEditingOperationStatus.PropertyTypeNotFound;
@@ -424,10 +427,16 @@ internal abstract class ContentEditingServiceBase<TContent, TContentType, TConte
content.SetCultureName(name, culture);
}
}
else if (contentType.VariesBySegment())
{
// this should be validated already so it's OK to throw an exception here
content.Name = contentEditingModelBase.Variants.FirstOrDefault(v => v.Segment is null)?.Name
?? throw new ArgumentException("Could not find the default segment variant", nameof(contentEditingModelBase));
}
else
{
// this should be validated already so it's OK to throw an exception here
content.Name = contentEditingModelBase.InvariantName
content.Name = contentEditingModelBase.Variants.FirstOrDefault(v => v.Culture is null && v.Segment is null)?.Name
?? throw new ArgumentException("Could not find a culture invariant variant", nameof(contentEditingModelBase));
}
}
@@ -437,26 +446,19 @@ internal abstract class ContentEditingServiceBase<TContent, TContentType, TConte
// create a mapping dictionary for all content type property types by their property aliases
Dictionary<string, IPropertyType> propertyTypesByAlias = GetPropertyTypesByAlias(contentType);
// flatten the invariant and variant property values from the model into one array, and remove any properties
// that do not exist on the content type
var propertyValues = contentEditingModelBase
.InvariantProperties
.Select(pv => new { Culture = (string?)null, Segment = (string?)null, Alias = pv.Alias, Value = pv.Value })
.Union(contentEditingModelBase
.Variants
.SelectMany(v => v
.Properties
.Select(vpv => new { Culture = v.Culture, Segment = v.Segment, Alias = vpv.Alias, Value = vpv.Value })))
// remove any properties that do not exist on the content type
PropertyValueModel[] propertyValues = contentEditingModelBase
.Properties
.Where(propertyValue => propertyTypesByAlias.ContainsKey(propertyValue.Alias))
.ToArray();
// update all properties on the content item
foreach (var propertyValue in propertyValues)
foreach (PropertyValueModel propertyValue in propertyValues)
{
// the following checks should already have been validated by now, so it's OK to throw exceptions here
if(propertyTypesByAlias.TryGetValue(propertyValue.Alias, out IPropertyType? propertyType) == false
|| (propertyType.VariesByCulture() && propertyValue.Culture.IsNullOrWhiteSpace())
|| (propertyType.VariesBySegment() && propertyValue.Segment.IsNullOrWhiteSpace()))
|| (propertyType.VariesBySegment() is false && propertyValue.Segment.IsNullOrWhiteSpace() is false))
{
throw new ArgumentException($"Culture or segment variance mismatch for property: {propertyValue.Alias}", nameof(contentEditingModelBase));
}
@@ -472,8 +474,8 @@ internal abstract class ContentEditingServiceBase<TContent, TContentType, TConte
// create a mapping dictionary for all content type property types by their property aliases
Dictionary<string, IPropertyType> propertyTypesByAlias = GetPropertyTypesByAlias(contentType);
var knownPropertyAliases = contentEditingModelBase
.InvariantProperties.Select(pv => pv.Alias)
.Union(contentEditingModelBase.Variants.SelectMany(v => v.Properties.Select(vpv => vpv.Alias)))
.Properties
.Select(pv => pv.Alias)
.Distinct()
.ToArray();
@@ -238,28 +238,33 @@ internal sealed class ContentPublishingService : IContentPublishingService
private async Task<ContentValidationResult> ValidateCurrentContentAsync(IContent content, string[] cultures)
{
IEnumerable<string?> effectiveCultures = content.ContentType.VariesByCulture()
? cultures.Union([null])
: [null];
// Would be better to be able to use a mapper/factory, but currently all that functionality is very much presentation logic.
var model = new ContentUpdateModel()
{
InvariantName = content.Name,
// NOTE KJA: this needs redoing; we need to make an informed decision whether to include invariant properties, depending on if editing invariant properties is allowed on all variants, or if the default language is included in cultures
InvariantProperties = content.Properties.Where(x => x.PropertyType.VariesByCulture() is false).Select(x => new PropertyValueModel()
{
Alias = x.Alias,
Value = x.GetValue()
}),
Properties = effectiveCultures.SelectMany(culture =>
content.Properties.Select(property => property.PropertyType.VariesByCulture() == (culture is not null)
? new PropertyValueModel
{
Alias = property.Alias,
Value = property.GetValue(culture: culture, segment: null, published: false),
Culture = culture
}
: null)
.WhereNotNull())
.ToArray(),
Variants = cultures.Select(culture => new VariantModel()
{
Name = content.GetPublishName(culture) ?? string.Empty,
Culture = culture,
Segment = null,
Properties = content.Properties.Where(prop => prop.PropertyType.VariesByCulture()).Select(prop => new PropertyValueModel()
{
Alias = prop.Alias,
Value = prop.GetValue(culture: culture, segment: null, published: false)
})
})
Segment = null
}).ToArray()
};
IContentType? contentType = _contentTypeService.Get(content.ContentType.Key)!;
ContentValidationResult validationResult = await _contentValidationService.ValidatePropertiesAsync(model, contentType, cultures);
return validationResult;
@@ -322,6 +322,15 @@ internal abstract class ContentTypeEditingServiceBase<TContentType, TContentType
// get the content type keys we want to use for compositions
Guid[] compositionKeys = KeysForCompositionTypes(model, CompositionType.Composition);
// if the content type keys are already set as compositions, don't perform any additional validation
// - this covers an edge case where compositions are configured for a content type before child content types are created
if (contentType is not null && contentType.ContentTypeComposition
.Select(c => c.Key)
.ContainsAll(compositionKeys))
{
return ContentTypeOperationStatus.Success;
}
// verify that all compositions keys are allowed
Guid[] allowedCompositionKeys = _contentTypeService.GetAvailableCompositeContentTypes(contentType, allContentTypeCompositions, isElement: model.IsElement)
.Results
@@ -31,9 +31,17 @@ internal abstract class ContentValidationServiceBase<TContentType>
IPropertyType[] contentTypePropertyTypes = contentType.CompositionPropertyTypes.ToArray();
IPropertyType[] invariantPropertyTypes = contentTypePropertyTypes
.Where(propertyType => propertyType.VariesByNothing())
.Where(propertyType => propertyType.Variations == ContentVariation.Nothing)
.ToArray();
IPropertyType[] cultureVariantPropertyTypes = contentTypePropertyTypes
.Where(propertyType => propertyType.Variations == ContentVariation.Culture)
.ToArray();
IPropertyType[] segmentVariantPropertyTypes = contentTypePropertyTypes
.Where(propertyType => propertyType.Variations == ContentVariation.Segment)
.ToArray();
IPropertyType[] cultureAndSegmentVariantPropertyTypes = contentTypePropertyTypes
.Where(propertyType => propertyType.Variations == ContentVariation.CultureAndSegment)
.ToArray();
IPropertyType[] variantPropertyTypes = contentTypePropertyTypes.Except(invariantPropertyTypes).ToArray();
var cultures = culturesToValidate?.WhereNotNull().Except(["*"]).ToArray();
if (cultures?.Any() is not true)
@@ -42,11 +50,14 @@ internal abstract class ContentValidationServiceBase<TContentType>
}
// we don't have any managed segments, so we have to make do with the ones passed in the model
var segments = contentEditingModelBase.Variants
.Where(variant => variant.Culture is null || cultures.Contains(variant.Culture))
.DistinctBy(variant => variant.Segment).Select(variant => variant.Segment)
.WhereNotNull()
.ToArray();
var segments =
new string?[] { null }
.Union(contentEditingModelBase.Variants
.Where(variant => variant.Culture is null || cultures.Contains(variant.Culture))
.DistinctBy(variant => variant.Segment).Select(variant => variant.Segment)
.WhereNotNull()
)
.ToArray();
foreach (IPropertyType propertyType in invariantPropertyTypes)
{
@@ -55,16 +66,45 @@ internal abstract class ContentValidationServiceBase<TContentType>
Culture = null, Segment = null, CulturesBeingValidated = cultures, SegmentsBeingValidated = segments
};
PropertyValueModel? propertyValueModel = contentEditingModelBase.InvariantProperties.FirstOrDefault(propertyValue => propertyValue.Alias == propertyType.Alias);
PropertyValueModel? propertyValueModel = contentEditingModelBase
.Properties
.FirstOrDefault(propertyValue => propertyValue.Alias == propertyType.Alias && propertyValue.Culture is null && propertyValue.Segment is null);
validationErrors.AddRange(ValidateProperty(propertyType, propertyValueModel, validationContext));
}
if (variantPropertyTypes.Any() is false)
foreach (IPropertyType propertyType in cultureVariantPropertyTypes)
{
return new ContentValidationResult { ValidationErrors = validationErrors };
foreach (var culture in cultures)
{
var validationContext = new PropertyValidationContext
{
Culture = culture, Segment = null, CulturesBeingValidated = cultures, SegmentsBeingValidated = segments
};
PropertyValueModel? propertyValueModel = contentEditingModelBase
.Properties
.FirstOrDefault(propertyValue => propertyValue.Alias == propertyType.Alias && propertyValue.Culture.InvariantEquals(culture) && propertyValue.Segment is null);
validationErrors.AddRange(ValidateProperty(propertyType, propertyValueModel, validationContext));
}
}
foreach (IPropertyType propertyType in variantPropertyTypes)
foreach (IPropertyType propertyType in segmentVariantPropertyTypes)
{
foreach (var segment in segments)
{
var validationContext = new PropertyValidationContext
{
Culture = null, Segment = segment, CulturesBeingValidated = cultures, SegmentsBeingValidated = segments
};
PropertyValueModel? propertyValueModel = contentEditingModelBase
.Properties
.FirstOrDefault(propertyValue => propertyValue.Alias == propertyType.Alias && propertyValue.Culture is null && propertyValue.Segment.InvariantEquals(segment));
validationErrors.AddRange(ValidateProperty(propertyType, propertyValueModel, validationContext));
}
}
foreach (IPropertyType propertyType in cultureAndSegmentVariantPropertyTypes)
{
foreach (var culture in cultures)
{
@@ -76,10 +116,8 @@ internal abstract class ContentValidationServiceBase<TContentType>
};
PropertyValueModel? propertyValueModel = contentEditingModelBase
.Variants
.FirstOrDefault(variant => string.Equals(variant.Culture, culture, StringComparison.InvariantCultureIgnoreCase) && string.Equals(segment, variant.Segment, StringComparison.InvariantCultureIgnoreCase))?
.Properties
.FirstOrDefault(propertyValue => propertyValue.Alias == propertyType.Alias);
.FirstOrDefault(propertyValue => propertyValue.Alias == propertyType.Alias && propertyValue.Culture.InvariantEquals(culture) && propertyValue.Segment.InvariantEquals(segment));
validationErrors.AddRange(ValidateProperty(propertyType, propertyValueModel, validationContext));
}
}
@@ -8,6 +8,8 @@ public interface IContentBlueprintEditingService
{
Task<IContent?> GetAsync(Guid key);
Task<IContent?> GetScaffoldedAsync(Guid key) => Task.FromResult<IContent?>(null);
Task<Attempt<PagedModel<IContent>?, ContentEditingOperationStatus>> GetPagedByContentTypeAsync(
Guid contentTypeKey,
int skip,
@@ -0,0 +1,7 @@
using Umbraco.Cms.Core.Models;
namespace Umbraco.Cms.Core.Services;
public interface IContentSearchService : IContentSearchService<IContent>
{
}
@@ -0,0 +1,14 @@
using Umbraco.Cms.Core.Models;
namespace Umbraco.Cms.Core.Services;
public interface IContentSearchService<TContent>
where TContent : class, IContentBase
{
Task<PagedModel<TContent>> SearchChildrenAsync(
string? query,
Guid? parentId,
Ordering? ordering,
int skip = 0,
int take = 100);
}
@@ -25,6 +25,7 @@ public interface IIndexedEntitySearchService
=> Search(objectType,query, skip, take, ignoreUserStartNodes);
// default implementation to avoid breaking changes falls back to old behaviour
[Obsolete("Please use the async version of this method, SearchAsync. Scheduled for removal in V17.")]
PagedModel<IEntitySlim> Search(
UmbracoObjectTypes objectType,
string query,
@@ -35,4 +36,14 @@ public interface IIndexedEntitySearchService
int take = 100,
bool ignoreUserStartNodes = false)
=> Search(objectType,query, skip, take, ignoreUserStartNodes);
Task<PagedModel<IEntitySlim>> SearchAsync(
UmbracoObjectTypes objectType,
string query,
Guid? parentId,
IEnumerable<Guid>? contentTypeIds,
bool? trashed,
int skip = 0,
int take = 100,
bool ignoreUserStartNodes = false);
}
@@ -0,0 +1,7 @@
using Umbraco.Cms.Core.Models;
namespace Umbraco.Cms.Core.Services;
public interface IMediaSearchService : IContentSearchService<IMedia>
{
}
@@ -123,8 +123,7 @@ internal sealed class MemberContentEditingService
var sensitivePropertyAliases = memberType.GetSensitivePropertyTypeAliases().ToArray();
return updateModel
.InvariantProperties
.Union(updateModel.Variants.SelectMany(variant => variant.Properties))
.Properties
.Select(property => property.Alias)
.Intersect(sensitivePropertyAliases, StringComparer.OrdinalIgnoreCase)
.Any() is false;
@@ -6,6 +6,7 @@ public enum ContentEditingOperationStatus
CancelledByNotification,
ContentTypeNotFound,
ContentTypeCultureVarianceMismatch,
ContentTypeSegmentVarianceMismatch,
NotFound,
ParentNotFound,
ParentInvalid,
@@ -1,5 +1,3 @@
using System.Globalization;
using System.Runtime.InteropServices;
using Microsoft.Extensions.Logging;
using Umbraco.Cms.Core.Events;
using Umbraco.Cms.Core.Models;
@@ -67,19 +65,14 @@ internal sealed class PublicAccessService : RepositoryService, IPublicAccessServ
{
// Get all ids in the path for the content item and ensure they all
// parse to ints that are not -1.
var ids = contentPath.Split(Constants.CharArrays.Comma, StringSplitOptions.RemoveEmptyEntries)
.Select(x => int.TryParse(x, NumberStyles.Integer, CultureInfo.InvariantCulture, out var val) ? val : -1)
.Where(x => x != -1)
.ToList();
// start with the deepest id
ids.Reverse();
// Start with the deepest id.
IEnumerable<int> ids = contentPath.GetIdsFromPathReversed().Where(x => x != -1);
using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
{
// This will retrieve from cache!
var entries = _publicAccessRepository.GetMany().ToList();
foreach (var id in CollectionsMarshal.AsSpan(ids))
foreach (var id in ids)
{
PublicAccessEntry? found = entries.Find(x => x.ProtectedNodeId == id);
if (found != null)
@@ -286,7 +279,7 @@ internal sealed class PublicAccessService : RepositoryService, IPublicAccessServ
return Attempt.FailWithStatus(PublicAccessOperationStatus.NoAllowedEntities, result);
}
if(entry.MemberUserNames.Any() && entry.MemberGroupNames.Any())
if (entry.MemberUserNames.Any() && entry.MemberGroupNames.Any())
{
return Attempt.FailWithStatus(PublicAccessOperationStatus.AmbiguousRule, result);
}
@@ -81,6 +81,8 @@ public static partial class UmbracoBuilderExtensions
builder.Services.AddUnique<IContentTypeSearchService, ContentTypeSearchService>();
builder.Services.AddUnique<IIndexedEntitySearchService, IndexedEntitySearchService>();
builder.Services.TryAddTransient<IReservedFieldNamesService, ReservedFieldNamesService>();
builder.Services.AddUnique<IContentSearchService, ContentSearchService>();
builder.Services.AddUnique<IMediaSearchService, MediaSearchService>();
return builder;
}
@@ -109,7 +109,7 @@ namespace Umbraco.Extensions
.Enrich.FromLogContext(); // allows us to dynamically enrich
logConfig.WriteTo.UmbracoFile(
path: umbracoFileConfiguration.GetPath(loggingConfiguration.LogDirectory),
path: umbracoFileConfiguration.GetPath(loggingConfiguration.LogDirectory, loggingConfiguration.LogFileNameFormat, loggingConfiguration.GetLogFileNameFormatArguments()),
fileSizeLimitBytes: umbracoFileConfiguration.FileSizeLimitBytes,
restrictedToMinimumLevel: umbracoFileConfiguration.RestrictedToMinimumLevel,
rollingInterval: umbracoFileConfiguration.RollingInterval,
@@ -1,6 +1,7 @@
using Microsoft.Extensions.Configuration;
using Serilog;
using Serilog.Events;
using Umbraco.Cms.Core.Logging;
namespace Umbraco.Cms.Infrastructure.Logging.Serilog;
@@ -43,5 +44,8 @@ public class UmbracoFileConfiguration
public int RetainedFileCountLimit { get; set; } = 31;
public string GetPath(string logDirectory) =>
Path.Combine(logDirectory, $"UmbracoTraceLog.{Environment.MachineName}..json");
GetPath(logDirectory, LoggingConfiguration.DefaultLogFileNameFormat, Environment.MachineName);
public string GetPath(string logDirectory, string fileNameFormat, params string[] fileNameArgs) =>
Path.Combine(logDirectory, string.Format(fileNameFormat, fileNameArgs));
}
@@ -27,11 +27,11 @@ internal class AccessDto
[ForeignKey(typeof(NodeDto), Name = "FK_umbracoAccess_umbracoNode_id2")]
public int NoAccessNodeId { get; set; }
[Column("createDate")]
[Column("createDate", ForceToUtc = false)]
[Constraint(Default = SystemMethods.CurrentDateTime)]
public DateTime CreateDate { get; set; }
[Column("updateDate")]
[Column("updateDate", ForceToUtc = false)]
[Constraint(Default = SystemMethods.CurrentDateTime)]
public DateTime UpdateDate { get; set; }
@@ -25,11 +25,11 @@ internal class AccessRuleDto
[Column("ruleType")]
public string? RuleType { get; set; }
[Column("createDate")]
[Column("createDate", ForceToUtc = false)]
[Constraint(Default = SystemMethods.CurrentDateTime)]
public DateTime CreateDate { get; set; }
[Column("updateDate")]
[Column("updateDate", ForceToUtc = false)]
[Constraint(Default = SystemMethods.CurrentDateTime)]
public DateTime UpdateDate { get; set; }
}
@@ -30,7 +30,7 @@ internal class AuditEntryDto
[Length(Constants.Audit.IpLength)]
public string? PerformingIp { get; set; }
[Column("eventDateUtc")]
[Column("eventDateUtc", ForceToUtc = false)]
[Constraint(Default = SystemMethods.CurrentDateTime)]
public DateTime EventDateUtc { get; set; }
@@ -29,7 +29,7 @@ public class ConsentDto
[Length(512)]
public string? Action { get; set; }
[Column("createDate")]
[Column("createDate", ForceToUtc = false)]
[Constraint(Default = SystemMethods.CurrentDateTime)]
public DateTime CreateDate { get; set; }
@@ -24,6 +24,7 @@ internal class ContentScheduleDto
[NullSetting(NullSetting = NullSettings.Null)] // can be invariant
public int? LanguageId { get; set; }
// NOTE: this date is explicitly stored and treated as UTC despite the lack of "Utc" postfix.
[Column("date")]
public DateTime Date { get; set; }
@@ -27,6 +27,6 @@ internal class ContentVersionCleanupPolicyDto
[NullSetting(NullSetting = NullSettings.Null)]
public int? KeepLatestVersionPerDayForDays { get; set; }
[Column("updated")]
[Column("updated", ForceToUtc = false)]
public DateTime Updated { get; set; }
}
@@ -33,7 +33,7 @@ internal class ContentVersionCultureVariationDto
[Column("name")]
public string? Name { get; set; }
[Column("date")] // TODO: db rename to 'updateDate'
[Column("date", ForceToUtc = false)] // TODO: db rename to 'updateDate'
public DateTime UpdateDate { get; set; }
[Column("availableUserId")] // TODO: db rename to 'updateDate'
@@ -22,7 +22,7 @@ public class ContentVersionDto
[Index(IndexTypes.NonClustered, Name = "IX_" + TableName + "_NodeId", ForColumns = "nodeId,current", IncludeColumns = "id,versionDate,text,userId,preventCleanup")]
public int NodeId { get; set; }
[Column("versionDate")] // TODO: db rename to 'updateDate'
[Column("versionDate", ForceToUtc = false)] // TODO: db rename to 'updateDate'
[Constraint(Default = SystemMethods.CurrentDateTime)]
public DateTime VersionDate { get; set; }
@@ -27,7 +27,7 @@ public class CreatedPackageSchemaDto
[NullSetting(NullSetting = NullSettings.NotNull)]
public string Value { get; set; } = null!;
[Column("updateDate")]
[Column("updateDate", ForceToUtc = false)]
[Constraint(Default = SystemMethods.CurrentDateTime)]
public DateTime UpdateDate { get; set; }
@@ -20,6 +20,6 @@ internal class DocumentPublishedReadOnlyDto
[Column("newest")]
public bool Newest { get; set; }
[Column("updateDate")]
[Column("updateDate", ForceToUtc = false)]
public DateTime VersionDate { get; set; }
}
@@ -42,7 +42,7 @@ internal class ExternalLoginDto
[Index(IndexTypes.NonClustered, ForColumns = "loginProvider,providerKey", Name = "IX_" + TableName + "_ProviderKey")]
public string ProviderKey { get; set; } = null!;
[Column("createDate")]
[Column("createDate", ForceToUtc = false)]
[Constraint(Default = SystemMethods.CurrentDateTime)]
public DateTime CreateDate { get; set; }
@@ -31,7 +31,7 @@ internal class ExternalLoginTokenDto
[NullSetting(NullSetting = NullSettings.NotNull)]
public string Value { get; set; } = null!;
[Column("createDate")]
[Column("createDate", ForceToUtc = false)]
[Constraint(Default = SystemMethods.CurrentDateTime)]
public DateTime CreateDate { get; set; }
@@ -23,7 +23,7 @@ internal class KeyValueDto
[NullSetting(NullSetting = NullSettings.Null)]
public string? Value { get; set; }
[Column("updated")]
[Column("updated", ForceToUtc = false)]
[Constraint(Default = SystemMethods.CurrentDateTime)]
public DateTime UpdateDate { get; set; }
@@ -35,7 +35,7 @@ internal class LogDto
[NullSetting(NullSetting = NullSettings.Null)]
public string? EntityType { get; set; }
[Column("Datestamp")]
[Column("Datestamp", ForceToUtc = false)]
[Constraint(Default = SystemMethods.CurrentDateTime)]
[Index(IndexTypes.NonClustered, Name = "IX_" + TableName + "_datestamp", ForColumns = "Datestamp,userId,NodeId")]
public DateTime Datestamp { get; set; }
@@ -45,7 +45,7 @@ internal class MemberDto
[Length(255)]
public string? SecurityStampToken { get; set; }
[Column("emailConfirmedDate")]
[Column("emailConfirmedDate", ForceToUtc = false)]
[NullSetting(NullSetting = NullSettings.Null)]
public DateTime? EmailConfirmedDate { get; set; }
@@ -62,15 +62,15 @@ internal class MemberDto
[Constraint(Default = 1)]
public bool IsApproved { get; set; }
[Column("lastLoginDate")]
[Column("lastLoginDate", ForceToUtc = false)]
[NullSetting(NullSetting = NullSettings.Null)]
public DateTime? LastLoginDate { get; set; }
[Column("lastLockoutDate")]
[Column("lastLockoutDate", ForceToUtc = false)]
[NullSetting(NullSetting = NullSettings.Null)]
public DateTime? LastLockoutDate { get; set; }
[Column("lastPasswordChangeDate")]
[Column("lastPasswordChangeDate", ForceToUtc = false)]
[NullSetting(NullSetting = NullSettings.Null)]
public DateTime? LastPasswordChangeDate { get; set; }
@@ -70,7 +70,7 @@ public class NodeDto
[Index(IndexTypes.NonClustered, Name = "IX_" + TableName + "_ObjectType", ForColumns = "nodeObjectType,trashed", IncludeColumns = "uniqueId,parentId,level,path,sortOrder,nodeUser,text,createDate")]
public Guid? NodeObjectType { get; set; }
[Column("createDate")]
[Column("createDate", ForceToUtc = false)]
[Constraint(Default = SystemMethods.CurrentDateTime)]
public DateTime CreateDate { get; set; }
}
@@ -27,7 +27,7 @@ internal class RelationDto
[ForeignKey(typeof(RelationTypeDto))]
public int RelationType { get; set; }
[Column("datetime")]
[Column("datetime", ForceToUtc = false)]
[Constraint(Default = SystemMethods.CurrentDateTime)]
public DateTime Datetime { get; set; }
@@ -23,11 +23,11 @@ internal class ServerRegistrationDto
[Index(IndexTypes.UniqueNonClustered, Name = "IX_computerName")] // server identity is unique
public string? ServerIdentity { get; set; }
[Column("registeredDate")]
[Column("registeredDate", ForceToUtc = false)]
[Constraint(Default = SystemMethods.CurrentDateTime)]
public DateTime DateRegistered { get; set; }
[Column("lastNotifiedDate")]
[Column("lastNotifiedDate", ForceToUtc = false)]
public DateTime DateAccessed { get; set; }
[Column("isActive")]
@@ -73,32 +73,32 @@ public class UserDto
[NullSetting(NullSetting = NullSettings.Null)]
public int? FailedLoginAttempts { get; set; }
[Column("lastLockoutDate")]
[Column("lastLockoutDate", ForceToUtc = false)]
[NullSetting(NullSetting = NullSettings.Null)]
public DateTime? LastLockoutDate { get; set; }
[Column("lastPasswordChangeDate")]
[Column("lastPasswordChangeDate", ForceToUtc = false)]
[NullSetting(NullSetting = NullSettings.Null)]
public DateTime? LastPasswordChangeDate { get; set; }
[Column("lastLoginDate")]
[Column("lastLoginDate", ForceToUtc = false)]
[NullSetting(NullSetting = NullSettings.Null)]
public DateTime? LastLoginDate { get; set; }
[Column("emailConfirmedDate")]
[Column("emailConfirmedDate", ForceToUtc = false)]
[NullSetting(NullSetting = NullSettings.Null)]
public DateTime? EmailConfirmedDate { get; set; }
[Column("invitedDate")]
[Column("invitedDate", ForceToUtc = false)]
[NullSetting(NullSetting = NullSettings.Null)]
public DateTime? InvitedDate { get; set; }
[Column("createDate")]
[Column("createDate", ForceToUtc = false)]
[NullSetting(NullSetting = NullSettings.NotNull)]
[Constraint(Default = SystemMethods.CurrentDateTime)]
public DateTime CreateDate { get; set; } = DateTime.Now;
[Column("updateDate")]
[Column("updateDate", ForceToUtc = false)]
[NullSetting(NullSetting = NullSettings.NotNull)]
[Constraint(Default = SystemMethods.CurrentDateTime)]
public DateTime UpdateDate { get; set; } = DateTime.Now;
@@ -44,12 +44,12 @@ public class UserGroupDto
[Obsolete("Is not used anymore Use UserGroup2PermissionDtos instead. This will be removed in Umbraco 18.")]
public string? DefaultPermissions { get; set; }
[Column("createDate")]
[Column("createDate", ForceToUtc = false)]
[NullSetting(NullSetting = NullSettings.NotNull)]
[Constraint(Default = SystemMethods.CurrentDateTime)]
public DateTime CreateDate { get; set; }
[Column("updateDate")]
[Column("updateDate", ForceToUtc = false)]
[NullSetting(NullSetting = NullSettings.NotNull)]
[Constraint(Default = SystemMethods.CurrentDateTime)]
public DateTime UpdateDate { get; set; }
@@ -1,4 +1,4 @@
using NPoco;
using NPoco;
using Umbraco.Cms.Core;
using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations;
@@ -24,7 +24,7 @@ internal class WebhookLogDto
[NullSetting(NullSetting = NullSettings.NotNull)]
public string StatusCode { get; set; } = string.Empty;
[Column(Name = "date")]
[Column(Name = "date", ForceToUtc = false)]
[Index(IndexTypes.NonClustered, Name = "IX_" + Constants.DatabaseSchema.Tables.WebhookLog + "_date")]
[NullSetting(NullSetting = NullSettings.NotNull)]
public DateTime Date { get; set; }
@@ -39,8 +39,11 @@ internal class ContentBaseFactory
content.CreatorId = nodeDto.UserId ?? Constants.Security.UnknownUserId;
content.WriterId = contentVersionDto.UserId ?? Constants.Security.UnknownUserId;
content.CreateDate = nodeDto.CreateDate;
content.UpdateDate = contentVersionDto.VersionDate;
// Dates stored in the database are local server time, but for SQL Server, will be considered
// as DateTime.Kind = Utc. Fix this so we are consistent when later mapping to DataTimeOffset.
content.CreateDate = DateTime.SpecifyKind(nodeDto.CreateDate, DateTimeKind.Local);
content.UpdateDate = DateTime.SpecifyKind(contentVersionDto.VersionDate, DateTimeKind.Local);
content.Published = dto.Published;
content.Edited = dto.Edited;
@@ -52,7 +55,7 @@ internal class ContentBaseFactory
content.PublishedVersionId = publishedVersionDto.Id;
if (dto.Published)
{
content.PublishDate = publishedVersionDto.ContentVersionDto.VersionDate;
content.PublishDate = DateTime.SpecifyKind(publishedVersionDto.ContentVersionDto.VersionDate, DateTimeKind.Local);
content.PublishName = publishedVersionDto.ContentVersionDto.Text;
content.PublisherId = publishedVersionDto.ContentVersionDto.UserId;
}
@@ -71,7 +74,7 @@ internal class ContentBaseFactory
}
/// <summary>
/// Builds an IMedia item from a dto and content type.
/// Builds a Media item from a dto and content type.
/// </summary>
public static Core.Models.Media BuildEntity(ContentDto dto, IMediaType? contentType)
{
@@ -97,8 +100,8 @@ internal class ContentBaseFactory
content.CreatorId = nodeDto.UserId ?? Constants.Security.UnknownUserId;
content.WriterId = contentVersionDto.UserId ?? Constants.Security.UnknownUserId;
content.CreateDate = nodeDto.CreateDate;
content.UpdateDate = contentVersionDto.VersionDate;
content.CreateDate = DateTime.SpecifyKind(nodeDto.CreateDate, DateTimeKind.Local);
content.UpdateDate = DateTime.SpecifyKind(contentVersionDto.VersionDate, DateTimeKind.Local);
// reset dirty initial properties (U4-1946)
content.ResetDirtyProperties(false);
@@ -111,7 +114,7 @@ internal class ContentBaseFactory
}
/// <summary>
/// Builds an IMedia item from a dto and content type.
/// Builds a Member item from a dto and member type.
/// </summary>
public static Member BuildEntity(MemberDto dto, IMemberType? contentType)
{
@@ -126,7 +129,9 @@ internal class ContentBaseFactory
content.Id = dto.NodeId;
content.SecurityStamp = dto.SecurityStampToken;
content.EmailConfirmedDate = dto.EmailConfirmedDate;
content.EmailConfirmedDate = dto.EmailConfirmedDate.HasValue
? DateTime.SpecifyKind(dto.EmailConfirmedDate.Value, DateTimeKind.Local)
: null;
content.PasswordConfiguration = dto.PasswordConfig;
content.Key = nodeDto.UniqueId;
content.VersionId = contentVersionDto.Id;
@@ -140,14 +145,20 @@ internal class ContentBaseFactory
content.CreatorId = nodeDto.UserId ?? Constants.Security.UnknownUserId;
content.WriterId = contentVersionDto.UserId ?? Constants.Security.UnknownUserId;
content.CreateDate = nodeDto.CreateDate;
content.UpdateDate = contentVersionDto.VersionDate;
content.CreateDate = DateTime.SpecifyKind(nodeDto.CreateDate, DateTimeKind.Local);
content.UpdateDate = DateTime.SpecifyKind(contentVersionDto.VersionDate, DateTimeKind.Local);
content.FailedPasswordAttempts = dto.FailedPasswordAttempts ?? default;
content.IsLockedOut = dto.IsLockedOut;
content.IsApproved = dto.IsApproved;
content.LastLoginDate = dto.LastLoginDate;
content.LastLockoutDate = dto.LastLockoutDate;
content.LastPasswordChangeDate = dto.LastPasswordChangeDate;
content.LastLockoutDate = dto.LastLockoutDate.HasValue
? DateTime.SpecifyKind(dto.LastLockoutDate.Value, DateTimeKind.Local)
: null;
content.LastLoginDate = dto.LastLoginDate.HasValue
? DateTime.SpecifyKind(dto.LastLoginDate.Value, DateTimeKind.Local)
: null;
content.LastPasswordChangeDate = dto.LastPasswordChangeDate.HasValue
? DateTime.SpecifyKind(dto.LastPasswordChangeDate.Value, DateTimeKind.Local)
: null;
// reset dirty initial properties (U4-1946)
content.ResetDirtyProperties(false);
@@ -186,7 +197,7 @@ internal class ContentBaseFactory
new ContentScheduleDto
{
Action = x.Action.ToString(),
Date = x.Date,
Date = DateTime.SpecifyKind(x.Date, DateTimeKind.Local),
NodeId = entity.Id,
LanguageId = languageRepository.GetIdByIsoCode(x.Culture, false),
Id = x.Id,
@@ -261,7 +272,7 @@ internal class ContentBaseFactory
UserId = entity.CreatorId,
Text = entity.Name,
NodeObjectType = objectType,
CreateDate = entity.CreateDate,
CreateDate = DateTime.SpecifyKind(entity.CreateDate, DateTimeKind.Local),
};
return dto;
@@ -275,7 +286,7 @@ internal class ContentBaseFactory
{
Id = entity.VersionId,
NodeId = entity.Id,
VersionDate = entity.UpdateDate,
VersionDate = DateTime.SpecifyKind(entity.UpdateDate, DateTimeKind.Local),
UserId = entity.WriterId,
Current = true, // always building the current one
Text = entity.Name,
@@ -39,16 +39,25 @@ internal static class UserFactory
user.Language = dto.UserLanguage;
user.SecurityStamp = dto.SecurityStampToken;
user.FailedPasswordAttempts = dto.FailedLoginAttempts ?? 0;
user.LastLockoutDate = dto.LastLockoutDate;
user.LastLoginDate = dto.LastLoginDate;
user.LastPasswordChangeDate = dto.LastPasswordChangeDate;
user.CreateDate = dto.CreateDate;
user.UpdateDate = dto.UpdateDate;
user.Avatar = dto.Avatar;
user.EmailConfirmedDate = dto.EmailConfirmedDate;
user.InvitedDate = dto.InvitedDate;
user.Kind = (UserKind)dto.Kind;
// Dates stored in the database are local server time, but for SQL Server, will be considered
// as DateTime.Kind = Utc. Fix this so we are consistent when later mapping to DataTimeOffset.
user.LastLockoutDate = dto.LastLockoutDate.HasValue
? DateTime.SpecifyKind(dto.LastLockoutDate.Value, DateTimeKind.Local)
: null;
user.LastLoginDate = dto.LastLoginDate.HasValue
? DateTime.SpecifyKind(dto.LastLoginDate.Value, DateTimeKind.Local)
: null;
user.LastPasswordChangeDate = dto.LastPasswordChangeDate.HasValue
? DateTime.SpecifyKind(dto.LastPasswordChangeDate.Value, DateTimeKind.Local)
: null;
user.CreateDate = DateTime.SpecifyKind(dto.CreateDate, DateTimeKind.Local);
user.UpdateDate = DateTime.SpecifyKind(dto.UpdateDate, DateTimeKind.Local);
// reset dirty initial properties (U4-1946)
user.ResetDirtyProperties(false);
@@ -29,7 +29,7 @@ internal class AuditRepository : EntityRepositoryBase<int, IAuditItem>, IAuditRe
List<LogDto>? dtos = Database.Fetch<LogDto>(sql);
return dtos.Select(x => new AuditItem(x.NodeId, Enum<AuditType>.Parse(x.Header), x.UserId ?? Constants.Security.UnknownUserId, x.EntityType, x.Comment, x.Parameters, x.Datestamp)).ToList();
return dtos.Select(x => new AuditItem(x.NodeId, Enum<AuditType>.Parse(x.Header), x.UserId ?? Constants.Security.UnknownUserId, x.EntityType, x.Comment, x.Parameters, DateTime.SpecifyKind(x.Datestamp, DateTimeKind.Local))).ToList();
}
public void CleanLogs(int maximumAgeOfLogsInMinutes)
@@ -104,12 +104,12 @@ internal class AuditRepository : EntityRepositoryBase<int, IAuditItem>, IAuditRe
totalRecords = page.TotalItems;
var items = page.Items.Select(
dto => new AuditItem(dto.NodeId, Enum<AuditType>.ParseOrNull(dto.Header) ?? AuditType.Custom, dto.UserId ?? Constants.Security.UnknownUserId, dto.EntityType, dto.Comment, dto.Parameters, dto.Datestamp)).ToList();
dto => new AuditItem(dto.NodeId, Enum<AuditType>.ParseOrNull(dto.Header) ?? AuditType.Custom, dto.UserId ?? Constants.Security.UnknownUserId, dto.EntityType, dto.Comment, dto.Parameters, DateTime.SpecifyKind(dto.Datestamp, DateTimeKind.Local))).ToList();
// map the DateStamp
for (var i = 0; i < items.Count; i++)
{
items[i].CreateDate = page.Items[i].Datestamp;
items[i].CreateDate = DateTime.SpecifyKind(page.Items[i].Datestamp, DateTimeKind.Local);
}
return items;
@@ -149,7 +149,7 @@ internal class AuditRepository : EntityRepositoryBase<int, IAuditItem>, IAuditRe
LogDto? dto = Database.First<LogDto>(sql);
return dto == null
? null
: new AuditItem(dto.NodeId, Enum<AuditType>.Parse(dto.Header), dto.UserId ?? Constants.Security.UnknownUserId, dto.EntityType, dto.Comment, dto.Parameters, dto.Datestamp);
: new AuditItem(dto.NodeId, Enum<AuditType>.Parse(dto.Header), dto.UserId ?? Constants.Security.UnknownUserId, dto.EntityType, dto.Comment, dto.Parameters, DateTime.SpecifyKind(dto.Datestamp, DateTimeKind.Local));
}
protected override IEnumerable<IAuditItem> PerformGetAll(params int[]? ids) => throw new NotImplementedException();
@@ -162,7 +162,7 @@ internal class AuditRepository : EntityRepositoryBase<int, IAuditItem>, IAuditRe
List<LogDto>? dtos = Database.Fetch<LogDto>(sql);
return dtos.Select(x => new AuditItem(x.NodeId, Enum<AuditType>.Parse(x.Header), x.UserId ?? Constants.Security.UnknownUserId, x.EntityType, x.Comment, x.Parameters, x.Datestamp)).ToList();
return dtos.Select(x => new AuditItem(x.NodeId, Enum<AuditType>.Parse(x.Header), x.UserId ?? Constants.Security.UnknownUserId, x.EntityType, x.Comment, x.Parameters, DateTime.SpecifyKind(x.Datestamp, DateTimeKind.Local))).ToList();
}
protected override Sql<ISqlContext> GetBaseQuery(bool isCount)
@@ -400,15 +400,17 @@ public class DocumentRepository : ContentRepositoryBase<int, IContent, DocumentR
{
foreach (ContentVariation v in contentVariation)
{
content.SetCultureInfo(v.Culture, v.Name, v.Date);
content.SetCultureInfo(v.Culture, v.Name, DateTime.SpecifyKind(v.Date, DateTimeKind.Local));
}
}
// Dates stored in the database are local server time, but for SQL Server, will be considered
// as DateTime.Kind = Utc. Fix this so we are consistent when later mapping to DataTimeOffset.
if (content.PublishedState is PublishedState.Published && content.PublishedVersionId > 0 && contentVariations.TryGetValue(content.PublishedVersionId, out contentVariation))
{
foreach (ContentVariation v in contentVariation)
{
content.SetPublishInfo(v.Culture, v.Name, v.Date);
content.SetPublishInfo(v.Culture, v.Name, DateTime.SpecifyKind(v.Date, DateTimeKind.Local));
}
}
@@ -1,3 +1,4 @@
using System.Data;
using NPoco;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Models;
@@ -98,6 +99,16 @@ internal class DocumentVersionRepository : IDocumentVersionRepository
Page<ContentVersionMeta>? page =
_scopeAccessor.AmbientScope?.Database.Page<ContentVersionMeta>(pageIndex + 1, pageSize, query);
// Dates stored in the database are local server time, but for SQL Server, will be considered
// as DateTime.Kind = Utc. Fix this so we are consistent when later mapping to DataTimeOffset.
if (page is not null)
{
foreach (ContentVersionMeta item in page.Items)
{
item.SpecifyVersionDateKind(DateTimeKind.Local);
}
}
totalRecords = page?.TotalItems ?? 0;
return page?.Items;
@@ -7,6 +7,7 @@ using Umbraco.Cms.Core.PropertyEditors.ValueConverters;
using Umbraco.Cms.Core.Serialization;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Strings;
using Umbraco.Extensions;
namespace Umbraco.Cms.Core.PropertyEditors;
@@ -263,6 +264,11 @@ public abstract class BlockValuePropertyValueEditorBase<TValue, TLayout> : DataV
bool canUpdateInvariantData,
HashSet<string> allowedCultures)
{
if (canUpdateInvariantData is false && targetValue is null)
{
return sourceValue;
}
BlockEditorData<TValue, TLayout>? source = BlockEditorValues.DeserializeAndClean(sourceValue);
BlockEditorData<TValue, TLayout>? target = BlockEditorValues.DeserializeAndClean(targetValue);
@@ -282,31 +288,29 @@ public abstract class BlockValuePropertyValueEditorBase<TValue, TLayout> : DataV
bool canUpdateInvariantData,
HashSet<string> allowedCultures)
{
source = UpdateSourceInvariantData(source, target, canUpdateInvariantData);
var mergedInvariant = UpdateSourceInvariantData(source, target, canUpdateInvariantData);
if (source is null && target is null)
// if the structure (invariant) is not defined after merger, the target content does not matter
if (mergedInvariant is null)
{
return null;
}
if (source is null && target?.Layout is not null)
// since we merged the invariant data (layout) before we get to this point
// we just need an empty valid object to run comparisons at this point
if (source is null)
{
source = new BlockEditorData<TValue, TLayout>([], CreateWithLayout(target.Layout));
}
else if (target is null && source?.Layout is not null)
{
target = new BlockEditorData<TValue, TLayout>([], CreateWithLayout(source.Layout));
source = new BlockEditorData<TValue, TLayout>([], new TValue());
}
// at this point the layout should have been merged or fallback created
if (source is null || target is null)
{
throw new ArgumentException("invalid sourceValue or targetValue");
}
// update the target with the merged invariant
target!.BlockValue.Layout = mergedInvariant.BlockValue.Layout;
// remove all the blocks that are no longer part of the layout
target.BlockValue.ContentData.RemoveAll(contentBlock =>
target.Layout!.Any(layoutItem => layoutItem.ReferencesContent(contentBlock.Key)) is false);
// remove any exposes that no longer have content assigned to them
target.BlockValue.Expose.RemoveAll(expose => target.BlockValue.ContentData.Any(data => data.Key == expose.ContentKey) is false);
target.BlockValue.SettingsData.RemoveAll(settingsBlock =>
target.Layout!.Any(layoutItem => layoutItem.ReferencesSetting(settingsBlock.Key)) is false);
@@ -314,16 +318,80 @@ public abstract class BlockValuePropertyValueEditorBase<TValue, TLayout> : DataV
CleanupVariantValues(source.BlockValue.ContentData, target.BlockValue.ContentData, canUpdateInvariantData, allowedCultures);
CleanupVariantValues(source.BlockValue.SettingsData, target.BlockValue.SettingsData, canUpdateInvariantData, allowedCultures);
// every source block value for a culture that is not allowed to be edited should be present on the target
RestoreMissingValues(
source.BlockValue.ContentData,
target.BlockValue.ContentData,
mergedInvariant.Layout!,
(layoutItem, itemData) => layoutItem.ContentKey == itemData.Key,
canUpdateInvariantData,
allowedCultures);
RestoreMissingValues(
source.BlockValue.SettingsData,
target.BlockValue.SettingsData,
mergedInvariant.Layout!,
(layoutItem, itemData) => layoutItem.SettingsKey == itemData.Key,
canUpdateInvariantData,
allowedCultures);
// update the expose list from source for any blocks that were restored
var missingSourceExposes =
source.BlockValue.Expose.Where(sourceExpose =>
target.BlockValue.Expose.Any(targetExpose => targetExpose.ContentKey == sourceExpose.ContentKey) is false
&& target.BlockValue.ContentData.Any(data => data.Key == sourceExpose.ContentKey)).ToList();
foreach (BlockItemVariation missingSourceExpose in missingSourceExposes)
{
target.BlockValue.Expose.Add(missingSourceExpose);
}
return target.BlockValue;
}
private void RestoreMissingValues(
List<BlockItemData> sourceBlockItemData,
List<BlockItemData> targetBlockItemData,
IEnumerable<TLayout> mergedLayout,
Func<TLayout, BlockItemData, bool> relevantBlockItemMatcher,
bool canUpdateInvariantData,
HashSet<string> allowedCultures)
{
IEnumerable<BlockItemData> blockItemsToCheck = sourceBlockItemData.Where(itemData =>
mergedLayout.Any(layoutItem => relevantBlockItemMatcher(layoutItem, itemData)));
foreach (BlockItemData blockItemData in blockItemsToCheck)
{
var relevantValues = blockItemData.Values.Where(value =>
(value.Culture is null && canUpdateInvariantData is false)
|| (value.Culture is not null && allowedCultures.Contains(value.Culture) is false)).ToList();
if (relevantValues.Count < 1)
{
continue;
}
BlockItemData targetBlockData =
targetBlockItemData.FirstOrDefault(itemData => itemData.Key == blockItemData.Key)
?? new BlockItemData(blockItemData.Key, blockItemData.ContentTypeKey, blockItemData.ContentTypeAlias);
foreach (BlockPropertyValue missingValue in relevantValues.Where(value => targetBlockData.Values.Any(targetValue =>
targetValue.Alias == value.Alias
&& targetValue.Culture == value.Culture
&& targetValue.Segment == value.Segment) is false))
{
targetBlockData.Values.Add(missingValue);
}
if (targetBlockItemData.Any(existingBlockItemData => existingBlockItemData.Key == targetBlockData.Key) is false)
{
targetBlockItemData.Add(blockItemData);
}
}
}
private void CleanupVariantValues(
List<BlockItemData> sourceBlockItems,
List<BlockItemData> targetBlockItems,
bool canUpdateInvariantData,
HashSet<string> allowedCultures)
{
// merge the source values into the target values for culture
// merge the source values into the target values per culture
foreach (BlockItemData targetBlockItem in targetBlockItems)
{
BlockItemData? sourceBlockItem = sourceBlockItems.FirstOrDefault(i => i.Key == targetBlockItem.Key);
@@ -190,7 +190,7 @@ public sealed class RichTextEditorPastedImages
});
}
img.SetAttributeValue("src", location);
img.SetAttributeValue("src", location ?? string.Empty);
// Remove the data attribute (so we do not re-process this)
img.Attributes.Remove(TemporaryImageDataAttribute);
@@ -1,11 +1,9 @@
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.Membership;
using Umbraco.Cms.Core.Persistence.Querying;
using Umbraco.Cms.Core.PropertyEditors;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Services.OperationStatus;
using Umbraco.Cms.Infrastructure.Persistence;
using Umbraco.Extensions;
namespace Umbraco.Cms.Infrastructure.Services;
@@ -17,24 +15,17 @@ internal abstract class ContentListViewServiceBase<TContent, TContentType, TCont
{
private readonly TContentTypeService _contentTypeService;
private readonly IDataTypeService _dataTypeService;
private readonly ISqlContext _sqlContext;
private readonly IContentSearchService<TContent> _contentSearchService;
protected ContentListViewServiceBase(TContentTypeService contentTypeService, IDataTypeService dataTypeService, ISqlContext sqlContext)
protected ContentListViewServiceBase(TContentTypeService contentTypeService, IDataTypeService dataTypeService, IContentSearchService<TContent> contentSearchService)
{
_contentTypeService = contentTypeService;
_dataTypeService = dataTypeService;
_sqlContext = sqlContext;
_contentSearchService = contentSearchService;
}
protected abstract Guid DefaultListViewKey { get; }
protected abstract Task<PagedModel<TContent>> GetPagedChildrenAsync(
int id,
IQuery<TContent>? filter,
Ordering? ordering,
int skip,
int take);
protected abstract Task<bool> HasAccessToListViewItemAsync(IUser user, Guid key);
protected async Task<Attempt<ListViewPagedModel<TContent>?, ContentCollectionOperationStatus>> GetListViewResultAsync(
@@ -62,7 +53,7 @@ internal abstract class ContentListViewServiceBase<TContent, TContentType, TCont
return Attempt.FailWithStatus<ListViewPagedModel<TContent>?, ContentCollectionOperationStatus>(orderingAttempt.Status, null);
}
PagedModel<TContent> items = await GetAllowedListViewItemsAsync(user, content?.Id ?? Constants.System.Root, filter, orderingAttempt.Result, skip, take);
PagedModel<TContent> items = await GetAllowedListViewItemsAsync(user, content?.Key, filter, orderingAttempt.Result, skip, take);
var result = new ListViewPagedModel<TContent>
{
@@ -216,20 +207,13 @@ internal abstract class ContentListViewServiceBase<TContent, TContentType, TCont
return await _dataTypeService.GetAsync(configuredListViewKey);
}
private async Task<PagedModel<TContent>> GetAllowedListViewItemsAsync(IUser user, int contentId, string? filter, Ordering? ordering, int skip, int take)
private async Task<PagedModel<TContent>> GetAllowedListViewItemsAsync(IUser user, Guid? contentId, string? filter, Ordering? ordering, int skip, int take)
{
var queryFilter = ParseQueryFilter(filter);
var pagedChildren = await GetPagedChildrenAsync(
contentId,
queryFilter,
ordering,
skip,
take);
PagedModel<TContent> pagedChildren = await _contentSearchService.SearchChildrenAsync(filter, contentId, ordering, skip, take);
// Filtering out child nodes after getting a paged result is an active choice here, even though the pagination might get off.
// This has been the case with this functionality in Umbraco for a long time.
var items = await FilterItemsBasedOnAccessAsync(user, pagedChildren.Items);
IEnumerable<TContent> items = await FilterItemsBasedOnAccessAsync(user, pagedChildren.Items);
var pagedResult = new PagedModel<TContent>
{
@@ -240,18 +224,6 @@ internal abstract class ContentListViewServiceBase<TContent, TContentType, TCont
return pagedResult;
}
private IQuery<TContent>? ParseQueryFilter(string? filter)
{
// Adding multiple conditions - considering key (as Guid) & name as filter param
Guid.TryParse(filter, out Guid filterAsGuid);
return filter.IsNullOrWhiteSpace()
? null
: _sqlContext.Query<TContent>()
.Where(c => (c.Name != null && c.Name.Contains(filter)) ||
c.Key == filterAsGuid);
}
// TODO: Optimize the way we filter out only the nodes the user is allowed to see - instead of checking one by one
private async Task<IEnumerable<TContent>> FilterItemsBasedOnAccessAsync(IUser user, IEnumerable<TContent> items)
{
@@ -21,9 +21,9 @@ internal sealed class ContentListViewService : ContentListViewServiceBase<IConte
IContentService contentService,
IContentTypeService contentTypeService,
IDataTypeService dataTypeService,
ISqlContext sqlContext,
IContentSearchService contentSearchService,
IContentPermissionAuthorizer contentPermissionAuthorizer)
: base(contentTypeService, dataTypeService, sqlContext)
: base(contentTypeService, dataTypeService, contentSearchService)
{
_contentService = contentService;
_contentPermissionAuthorizer = contentPermissionAuthorizer;
@@ -49,32 +49,6 @@ internal sealed class ContentListViewService : ContentListViewServiceBase<IConte
return await GetListViewResultAsync(user, content, dataTypeKey, orderBy, orderCulture, orderDirection, filter, skip, take);
}
protected override Task<PagedModel<IContent>> GetPagedChildrenAsync(
int id,
IQuery<IContent>? filter,
Ordering? ordering,
int skip,
int take)
{
PaginationHelper.ConvertSkipTakeToPaging(skip, take, out var pageNumber, out var pageSize);
IEnumerable<IContent> items = _contentService.GetPagedChildren(
id,
pageNumber,
pageSize,
out var total,
filter,
ordering);
var pagedResult = new PagedModel<IContent>
{
Items = items,
Total = total,
};
return Task.FromResult(pagedResult);
}
// We can use an authorizer here, as it already handles all the necessary checks for this filtering.
// However, we cannot pass in all the items; we want only the ones that comply, as opposed to
// a general response whether the user has access to all nodes.
@@ -0,0 +1,33 @@
using Microsoft.Extensions.Logging;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Persistence.Querying;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Infrastructure.Persistence;
namespace Umbraco.Cms.Infrastructure.Services.Implement;
internal sealed class ContentSearchService : ContentSearchServiceBase<IContent>, IContentSearchService
{
private readonly IContentService _contentService;
public ContentSearchService(ISqlContext sqlContext, IIdKeyMap idKeyMap, ILogger<ContentSearchService> logger, IContentService contentService)
: base(sqlContext, idKeyMap, logger)
=> _contentService = contentService;
protected override UmbracoObjectTypes ObjectType => UmbracoObjectTypes.Document;
protected override Task<IEnumerable<IContent>> SearchChildrenAsync(
IQuery<IContent>? query,
int parentId,
Ordering? ordering,
long pageNumber,
int pageSize,
out long total)
=> Task.FromResult(_contentService.GetPagedChildren(
parentId,
pageNumber,
pageSize,
out total,
query,
ordering));
}
@@ -0,0 +1,77 @@
using Microsoft.Extensions.Logging;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Persistence.Querying;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Infrastructure.Persistence;
using Umbraco.Extensions;
namespace Umbraco.Cms.Infrastructure.Services.Implement;
internal abstract class ContentSearchServiceBase<TContent> : IContentSearchService<TContent>
where TContent : class, IContentBase
{
private readonly ISqlContext _sqlContext;
private readonly IIdKeyMap _idKeyMap;
private readonly ILogger<ContentSearchServiceBase<TContent>> _logger;
protected ContentSearchServiceBase(ISqlContext sqlContext, IIdKeyMap idKeyMap, ILogger<ContentSearchServiceBase<TContent>> logger)
{
_sqlContext = sqlContext;
_idKeyMap = idKeyMap;
_logger = logger;
}
protected abstract UmbracoObjectTypes ObjectType { get; }
protected abstract Task<IEnumerable<TContent>> SearchChildrenAsync(
IQuery<TContent>? query,
int parentId,
Ordering? ordering,
long pageNumber,
int pageSize,
out long total);
public async Task<PagedModel<TContent>> SearchChildrenAsync(
string? query,
Guid? parentId,
Ordering? ordering,
int skip = 0,
int take = 100)
{
var parentIdAsInt = Constants.System.Root;
if (parentId.HasValue)
{
Attempt<int> keyToId = _idKeyMap.GetIdForKey(parentId.Value, ObjectType);
if (keyToId.Success is false)
{
_logger.LogWarning("Could not obtain an ID for parent key: {parentId} (object type: {contentType}", parentId, typeof(TContent).FullName);
return new PagedModel<TContent>(0, []);
}
parentIdAsInt = keyToId.Result;
}
PaginationHelper.ConvertSkipTakeToPaging(skip, take, out var pageNumber, out var pageSize);
IQuery<TContent>? contentQuery = ParseQuery(query);
IEnumerable<TContent> items = await SearchChildrenAsync(contentQuery, parentIdAsInt, ordering, pageNumber, pageSize, out var total);
return new PagedModel<TContent>
{
Items = items,
Total = total,
};
}
private IQuery<TContent>? ParseQuery(string? query)
{
// Adding multiple conditions - considering key (as Guid) & name as filter param
Guid.TryParse(query, out Guid filterAsGuid);
return query.IsNullOrWhiteSpace()
? null
: _sqlContext
.Query<TContent>()
.Where(c => (c.Name != null && c.Name.Contains(query)) || c.Key == filterAsGuid);
}
}
@@ -73,6 +73,17 @@ internal sealed class IndexedEntitySearchService : IIndexedEntitySearchService
int skip = 0,
int take = 100,
bool ignoreUserStartNodes = false)
=> SearchAsync(objectType, query, parentId, contentTypeIds, trashed, skip, take, ignoreUserStartNodes).GetAwaiter().GetResult();
public Task<PagedModel<IEntitySlim>> SearchAsync(
UmbracoObjectTypes objectType,
string query,
Guid? parentId,
IEnumerable<Guid>? contentTypeIds,
bool? trashed,
int skip = 0,
int take = 100,
bool ignoreUserStartNodes = false)
{
UmbracoEntityTypes entityType = objectType switch
{
@@ -115,12 +126,12 @@ internal sealed class IndexedEntitySearchService : IIndexedEntitySearchService
.Where(key => key != Guid.Empty)
.ToArray();
return new PagedModel<IEntitySlim>
return Task.FromResult(new PagedModel<IEntitySlim>
{
Items = keys.Any()
? _entityService.GetAll(objectType, keys)
: Enumerable.Empty<IEntitySlim>(),
Total = totalFound
};
});
}
}
@@ -1,11 +1,9 @@
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.Membership;
using Umbraco.Cms.Core.Persistence.Querying;
using Umbraco.Cms.Core.Security.Authorization;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Services.OperationStatus;
using Umbraco.Cms.Infrastructure.Persistence;
namespace Umbraco.Cms.Infrastructure.Services.Implement;
@@ -20,9 +18,9 @@ internal sealed class MediaListViewService : ContentListViewServiceBase<IMedia,
IMediaService mediaService,
IMediaTypeService mediaTypeService,
IDataTypeService dataTypeService,
ISqlContext sqlContext,
IMediaSearchService mediaSearchService,
IMediaPermissionAuthorizer mediaPermissionAuthorizer)
: base(mediaTypeService, dataTypeService, sqlContext)
: base(mediaTypeService, dataTypeService, mediaSearchService)
{
_mediaService = mediaService;
_mediaPermissionAuthorizer = mediaPermissionAuthorizer;
@@ -50,27 +48,6 @@ internal sealed class MediaListViewService : ContentListViewServiceBase<IMedia,
return await GetListViewResultAsync(user, media, dataTypeKey, orderBy, null, orderDirection, filter, skip, take);
}
protected override Task<PagedModel<IMedia>> GetPagedChildrenAsync(int id, IQuery<IMedia>? filter, Ordering? ordering, int skip, int take)
{
PaginationHelper.ConvertSkipTakeToPaging(skip, take, out var pageNumber, out var pageSize);
IEnumerable<IMedia> items = _mediaService.GetPagedChildren(
id,
pageNumber,
pageSize,
out var total,
filter,
ordering);
var pagedResult = new PagedModel<IMedia>
{
Items = items,
Total = total,
};
return Task.FromResult(pagedResult);
}
// We can use an authorizer here, as it already handles all the necessary checks for this filtering.
// However, we cannot pass in all the items; we want only the ones that comply, as opposed to
// a general response whether the user has access to all nodes.

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