Compare commits

..
180 Commits
Author SHA1 Message Date
Andy ButlandandCopilot 9b2fd1253b Optimize initialization of document URLs on start-up (#19498)
* Optimize initialization of document URLs on startup.

* Apply suggestions from code review

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

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-06-10 10:31:26 +02:00
Andy Butland 024697f3bf Bumped version to 15.4.3 2025-06-10 10:28:55 +02:00
Andy ButlandandGitHub d920e93d1e Merge commit from fork 2025-06-03 05:21:11 +02:00
Andy Butland 853c1acf7e Bumped version to 15.4.2 2025-05-20 13:10:02 +01:00
Andy ButlandandGitHub 0ad020f0ce Restored interface methods on obsolete IBackgroundTaskQueue (#19223)
* Restore interface methods on obsolete IBackgroundTaskQueue.

* Fixed typos in comment.
2025-05-05 07:38:31 +02:00
Andy Butland fc1455e0d8 Bumped version to 15.4.1. 2025-05-03 17:28:44 +02:00
Andy Butland 15a8b4066a Bumped version to 15.4.0. 2025-04-30 15:04:08 +02:00
Andy ButlandandGitHub 38f6e2b0da Updates Examine to latest patch release. (#19193) 2025-04-30 09:57:55 +02:00
Andy Butland 14f60a108a V13: Clear Member Username Cache in Load Balanced Environments (#19191)
* Clear usernamekey

* Odd explaining comment

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

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

* Make UserNameCachePrefix readonly for better immutabilityly

* Move prefix to CacheKeys constants

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
# Conflicts:
#	src/Umbraco.Core/Cache/CacheKeys.cs
2025-04-29 18:10:44 +02:00
Jacob Overgaard 375a0b4388 bump to 15.4.0-rc3 2025-04-29 15:34:59 +02:00
Jacob OvergaardandGitHub 6300ccc211 fix: The server path is not appended to stylesheets before load in Backoffice (#19192)
* build(deps): bump tiptap from 2.11.5 to 2.11.7

* fix: prepends the system `/css` folder to stylesheets before attempting to load them

* fix: adds more safety around path assumptions

* chore: eslint fix

* fix: prepend only the system path to picked stylesheets
2025-04-29 13:33:26 +00:00
Jacob Overgaard 5b6f544d2a build: restores some of the behavior from V13 in relation to StaticAssets (#19189)
In v13, the StaticAssets build was only triggered based on the existence of either the output folder or a preserve.* marker file. Here, we also additionally check for the node_modules/.package-lock.json file before reinstalling npm dependencies. We also now only run `npm install` rather than `npm ci` to optimise the build.
2025-04-29 13:10:29 +02:00
e932fa5404 Webhook log authorization and file system path checks (#19177)
* Add authorization for webhooks to item and log endpoints.

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

* Added missing usings.

* Revert changes to the webhook items API

---------

Co-authored-by: kjac <kja@umbraco.dk>
2025-04-28 12:10:43 +00:00
mole 83e580c3a7 Fix nuget publish stage 2025-04-25 13:31:34 +02:00
Andy Butlandandmole 31ee35e721 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:57 +02:00
MoleandKenn Jacobsen cf0f3f1380 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:18:53 +02:00
Sven Geusensandkjac 31c3f5ae0c Remove fake null checks as they are no longer needed after merge into v16 (#19109) 2025-04-24 12:28:54 +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
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
Jacob Overgaard 8b759cf3a4 bump version to 15.4.0-rc2 2025-04-16 14:47:55 +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
Jacob Overgaard cc9c33bfe6 fix: adds missing export 2025-04-15 12:57:57 +02:00
Jacob Overgaard 6edffd9f08 docs: fix import path 2025-04-15 12:23:37 +02:00
Andy Butland 981f173a79 Clarified "too many" entries validation message. (#19040) 2025-04-15 08:35:49 +02: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 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
Kenn JacobsenandGitHub 7495c3c7b2 Treat content schedule dates as UTC (#19028) 2025-04-14 14:50:02 +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
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
Niels LyngsøandGitHub 97b3023e14 make sure only to prepend relative URLs (#18998) 2025-04-10 14:56:14 +01:00
Niels LyngsøandGitHub ba0028c03d Feature: Use uui invalid color (Awaiting new UUI version) (#18273)
* display validation on save + use uui-color-invalid

* update css vars

* use standalone color for property layout

* remove color from label

* fix badge

* fix create button color

* clean up

* correct badge colors
2025-04-09 10:14:26 +00:00
6f38a57c8a Document permission inheritance in UI (#18935)
* check the full path for permissions

* fix race condition

* wip update permission when variants change

* Populate ancestor keys on document tree response items.

* Populate ancestor keys on document collection response items.

* Update OpenApi.json

* generate server models

* update types

* map data

* add ancestor context

* set ancestors in context

* use ancestor context in tree

* clean up

* provide ancestor context from a collection item

* provide ancestor context from structure context

* Use array of objects rather than Ids for the ancestor collection.

* Update OpenApi.json.

* add ancestor data to mocks

* set ancestors ids in mocks

* omit ancestors for recycle bin item

* use correct models for document blueprint mock data

* remove constructor

* mock documents for testing

* add user group permission test data

* wip document user permission condition tests

* generate new server models

* update data efter server models update

* clean up

* Update entity-actions-table-column-view.element.ts

* longer time for not found to appear

* use arg

* observe alias

* set new the right place

* remove const

---------

Co-authored-by: Andy Butland <abutland73@gmail.com>
Co-authored-by: Niels Lyngsø <nsl@umbraco.dk>
Co-authored-by: Niels Lyngsø <niels.lyngso@gmail.com>
2025-04-09 11:08:28 +02:00
Andy ButlandandGitHub a0e3ca601e Aggregate document permissions for current user in API response (#18721)
* Create integration test verifying existing behaviour.

* Aggregate permissions per document for the current user response.

* Refactoring following Codescene warnings.
2025-04-09 10:57:50 +02:00
1a9d8e3a5d Fixes label of enable/prevent clean up button after update (#18946)
* Update enable cleanup button text on toggle.

* Handle console error visible on cancel of rollback dialog.

* Return value from `umb-rollback-modal`

---------

Co-authored-by: leekelleher <leekelleher@gmail.com>
2025-04-09 08:15:00 +00:00
76f27726c5 V15: User password resetting notification (#18679)
* Introduce UserPasswordResettingNotification

* Removed changes to IUserService interface.

---------

Co-authored-by: Andy Butland <abutland73@gmail.com>
2025-04-09 09:28:12 +02:00
d9d66465f6 Allow save of empty translations for dictionary items (#18980)
* Allow save of empty translations for dictionary items.

* Updated Open API schema to match request model update

---------

Co-authored-by: kjac <kja@umbraco.dk>
2025-04-09 09:19:48 +02:00
947afdbc1e Amend root content routing and ensure trailing slashes as configured (#18958)
* Amend root content routing and ensure trailing slashes as configured

* Fix false positives at root + add more tests

* Awaited async method and resolved warning around readonly.

---------

Co-authored-by: Andy Butland <abutland73@gmail.com>
2025-04-09 07:46:24 +02:00
Lee KelleherandGitHub 134c8006c0 umb-localize encode HTML arguments (#18960)
* Moves `escapeHTML` call from localization controller to `umb-localize` element

* Adds supporting unit-test

* Removed unit-test

as it is now expected that the localization
controller will return literal HTML markup.

* Updated import path

* Removed extra call to `text()`
2025-04-08 12:32:36 +00:00
Andreas ZerbstandGitHub 86e7343f99 V15 QA added clipboard test for not being able to copy to root when block is not allowed at root (#18937)
* Added clipboard test

* Bumped version

* Updated to use the name

* Run all tests on the pipeline

* Reverted command
2025-04-08 09:53:07 +02:00
84d0ae3ea2 V15 QA Fixing the failing media acceptance tests (#18881)
* Fixed the function name due to test helper changes

* Updated assertion steps due to UI changes

* Added more waits

* Bumped version

* Increase timeout

* Reverted

---------

Co-authored-by: Andreas Zerbst <73799582+andr317c@users.noreply.github.com>
2025-04-08 12:41:32 +07:00
Andy Butland aa76f782ef Merge branch 'release/15.3.1' into v15/dev 2025-04-08 07:17:01 +02:00
Andy ButlandandGitHub 06a2a500b3 Merge commit from fork
* Prevent path traveral vulnerability with upload of temporary files.

* Used BadRequest instead of NotFound for invalid file name response.
2025-04-08 05:03:40 +02:00
Niels LyngsøandGitHub 17a5218a58 fix 18367 (#18956) 2025-04-07 18:45:22 +02:00
Niels LyngsøandGitHub 4c9084238c Feature: discard changes for block workspace (#18930)
* make getHasUnpersistedChanges public

* Discard changes impl for Block Workspace
2025-04-07 18:42:31 +02:00
9c03422222 Data type References UI: Workspace + Delete (#18914)
* Updated management API endpoint and model for data type references to align with that used for documents, media etc.

* Refactoring.

* Update src/Umbraco.Core/Constants-ReferenceTypes.cs

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

* Fixed typos.

* generate server models

* add extension slot

* register data type reference info app

* add reference data mappers

* Added id to tracked reference content type response.

* Updated OpenApi.json.

* Added missing updates.

* generate new models

* update models

* register ref item

* remove debugger

* render types

* register member type property type ref

* register media type property type ref

* Renamed model and constants from code review feedback.

* register reference workspace info app kind

* use kind for document references

* use kind for media references

* use kind for member references

* use deleteWithRelation kind when deleting data types

* fix manifest types

* fix types

* Update types.gen.ts

* update code to fit new server models

---------

Co-authored-by: Andy Butland <abutland73@gmail.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-04-07 16:24:29 +00:00
Sebastiaan Janssen b61b9dbd5e Merge branch 'v15/dev' into contrib 2025-04-07 16:58:22 +02:00
HenrikandGitHub d9a31d337a Avoid allocating strings for parsing comma separated int values (#18199) 2025-04-07 16:56:59 +02:00
Henrik GedionsenandSebastiaan Janssen 822c8374ef Avoid allocating new empty arrays, reuse existing empty array 2025-04-07 16:31:31 +02:00
Henrik GedionsenandSebastiaan Janssen 4239f0fc06 Remove unneeded double seek 2025-04-07 16:24:31 +02:00
Henrik GedionsenandSebastiaan Janssen db43799d0f Avoid some heap allocations 2025-04-07 16:23:45 +02:00
HenrikandGitHub 2723e4f77c Avoid unneeded Dictionary operations (#18890) 2025-04-07 15:02:28 +02:00
Jacob Welander JensenandGitHub f926316751 V15/fix/18595 (#18925)
* fix for #18595

* updates the en.ts
2025-04-07 14:53:01 +02:00
Mads RasmussenandGitHub b991976fd3 Fix modal route registration circular import (#18953)
* fix modal route registration circular import

* Update modal-route-registration.controller.ts
2025-04-07 11:39:09 +00:00
Mads RasmussenandGitHub c0f820a05c remove segment toggle for elements (#18949) 2025-04-07 11:41:27 +02:00
Mads RasmussenandGitHub f6d93ef0ba fix circular icon import (#18952) 2025-04-07 11:39:51 +02:00
Andy ButlandandGitHub 3d1e17b07f Integration tests for content publishing with ancestor unpublished (#18941)
* Resolved warnings in test class.

* Refactor regions into partial classes.

* Aligned test names.

* Variable name refactoring.

* Added tests for unpublished paths.

* Adjust tests to verify current behaviour.

* Cleaned up project file.
2025-04-07 10:04:11 +02:00
ae423decc3 Add raw value validation to multiple text strings property editor (#18936)
* Add raw value validation to multiple text strings property editor

* Added additional assert on unit test and comment on validation logic.

* Don't remove items to obtain a valid value

---------

Co-authored-by: Andy Butland <abutland73@gmail.com>
2025-04-07 09:40:17 +02:00
dependabot[bot]andJacob Overgaard 1d9a031c1c Bump vite from 6.2.4 to 6.2.5 in /src/Umbraco.Web.UI.Client
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 6.2.4 to 6.2.5.
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/v6.2.5/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v6.2.5/packages/vite)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-04-07 08:56:42 +02:00
Andy ButlandandGitHub 4078e83634 Look-up redirect in content finder for multi-lingual sites using path and legacy route prefixed with the integer ID of the node with domains defined (#18763)
* Look-up redirect in content finder for multi-lingual sites using path and legacy route prefixed with the integer ID of the node with domains defined.

* Added tests to verify functionality.

* Added reference to previous PR.

* Referenced second PR.

* Assemble URLs for all cultures, not just the default.

* Revert previous update.

* Display an original URL if we have one.
2025-04-07 08:18:06 +02:00
Nikolaj Geisle 4b016317f9 Skip lock tests 2025-04-04 17:34:08 +02:00
1f4c19d484 Updated management API endpoint and model for data type references to align with that used for documents, media etc. (#18905)
* Updated management API endpoint and model for data type references to align with that used for documents, media etc.

* Refactoring.

* Update src/Umbraco.Core/Constants-ReferenceTypes.cs

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

* Fixed typos.

* Added id to tracked reference content type response.

* Updated OpenApi.json.

* Added missing updates.

* Renamed model and constants from code review feedback.

* Fix typo

* Fix multiple enumeration

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: mole <nikolajlauridsen@protonmail.ch>
2025-04-04 15:10:06 +02:00
fd77074d57 Added management API endpoint, service and repository for retrieval of references from the recycle bin (#18882)
* Added management API endpoint, service and repository for retrieval of references from the recycle bin.

* Update src/Umbraco.Cms.Api.Management/Controllers/Document/RecycleBin/ReferencedByDocumentRecycleBinController.cs

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

* Removed unused code.

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-04-04 12:52:39 +00:00
Niels LyngsøandGitHub bf74636f26 make getHasUnpersistedChanges public (#18929) 2025-04-04 12:42:35 +00:00
Jacob Overgaard 1aff4c69f0 Merge remote-tracking branch 'origin/contrib' into v15/dev 2025-04-04 14:21:38 +02:00
Jacob Overgaard c139015019 removes autogenerated workflows 2025-04-04 14:10:45 +02:00
dependabot[bot]andJacob Overgaard 90a34e17ca Bump vite from 6.2.3 to 6.2.4 in /src/Umbraco.Web.UI.Client
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 6.2.3 to 6.2.4.
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/v6.2.4/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v6.2.4/packages/vite)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-04-04 14:05:44 +02:00
Niels LyngsøandGitHub 806f10e52a V15/bugfix/fix route issue from 18859 (#18931)
* unique check

* unique for workspace empty path

* more unique routes
2025-04-04 11:20:29 +02:00
Niels LyngsøandGitHub 788e5cd678 remove unnecessary code (#18927) 2025-04-04 11:18:30 +02:00
Andy ButlandandGitHub ebd5fb4d0c Fixes save of empty, invariant block list on variant content. (#18932) 2025-04-04 10:14:24 +02:00
Andy ButlandandGitHub 5f3ce28032 Clear roots before rebuilding navigation dictionary (#18766)
* Clear roots before rebuilding navigation dictionary.

* Added tests to verify fix.

* Correct test implementation.

* Convert integration tests with method overloads into test cases.

* Integration test compatibility supressions.
2025-04-04 09:14:48 +02:00
ad7053af36 Move publish with descendants to a background task with polling (#18497)
* Use background queue for database cache rebuild and track rebuilding status.

* Updated OpenApi.json and client-side types.

* Updated client to poll for completion of database rebuild.

* Move IBackgroundTaskQueue to core and prepare publish branch to run as background task.

* Endpoints for retrieval of status and result from branch publish operations.

* Poll and retrieve result for publish with descendants.

* Handled issues from testing.

* Rework to single controller for status and result.

* Updated client side sdk.

* OpenApi post dev merge gen

---------

Co-authored-by: Migaroez <geusens@gmail.com>
2025-04-04 07:42:26 +02:00
Andy ButlandandGitHub 6247f54976 Adds ancestor ID details on document tree and collection responses (#18909)
* Populate ancestor keys on document tree response items.

* Populate ancestor keys on document collection response items.

* Update OpenApi.json

* Use array of objects rather than Ids for the ancestor collection.

* Update OpenApi.json.
2025-04-04 06:51:29 +02:00
Andy ButlandandGitHub 3e6b9313e5 Only apply validation on content update to variant cultures where the editor has permission for the culture (#18778)
* Only apply validation on content update to variant cultures where the editor has permission for the culture.

* Remove inadvertent comment updates.

* Fixed failing integration test.
2025-04-03 22:09:40 +02:00
7d41791543 Ensure has children reflects only items with folder children when folders only are queried. (#18790)
* Ensure has children reflects only items with folder children when folders only are queried.

* Added supression for change to integration test public code.

---------

Co-authored-by: Migaroez <geusens@gmail.com>
2025-04-03 21:55:34 +02:00
Niels Lyngsø 157b7d14eb Merge branch 'v15/dev' into contrib 2025-04-03 21:32:22 +02:00
Lee KelleherandGitHub f3658bf356 Tiptap RTE: Style Menu extension kind (#18918)
* Adds 'styleMenu' Tiptap toolbar extension kind

* Adds icons for `<h4>` and `<p>` tags

* Adds commands to HTML Global Attributes extension

for setting the `class` and `id` attributes.

* Renamed "default-tiptap-toolbar-element.api.ts" file

The "element" part was confusing.

* Toolbar Menu: uses correct `item` value

* Cascading Menu: adds localization for the label

* Adds `label` attribute to UUI components

for accessibility.

* Toolbar Menu: uses correct `appearance` value

* Removed unrequired `api` from Style Select

* Destructs the `item.data` object
2025-04-03 11:44:57 +02:00
Andy ButlandandGitHub e4b3104399 Remove admin permission on user configuration, allowing users with user section access only to manaage users and groups. (#18848) 2025-04-03 10:51:45 +02:00
Jacob Overgaard 9db62a2c0c ci: add Azure Static Web Apps workflow file
on-behalf-of: @Azure opensource@microsoft.com
2025-04-03 09:07:16 +02:00
Jacob Overgaard 3cd6fcfe67 ci: add Azure Static Web Apps workflow file
on-behalf-of: @Azure opensource@microsoft.com
2025-04-03 09:03:45 +02:00
Jacob Overgaard 3b736d8c73 ci: add Azure Static Web Apps workflow file
on-behalf-of: @Azure opensource@microsoft.com
2025-04-03 09:01:41 +02:00
bbfe40d733 Fix issue preventing blueprint derived values from being scaffolded (#18917)
* Fix issue preventing blueprint derived values from being scaffolded.

* fix manipulating frooen array

* compare with variantId as well

---------

Co-authored-by: Niels Lyngsø <niels.lyngso@gmail.com>
2025-04-03 06:45:14 +02:00
Kenn JacobsenandGitHub 9d30d5b11c Fix pagination for users restricted by start nodes (#18907)
* Fix pagination for users restricted by start nodes

* Default implementation to avoid breakage

* Review comments

* Fix failing test

* Add media start node tests
2025-04-02 09:34:52 +02:00
Niels LyngsøandGitHub 11c19847cf Feature: highlight invariant doc with variant blocks is unsupported (#18806)
* mark variant blocks in invariant docs as invalid

* implement RTE Blocks
2025-04-02 11:25:34 +07:00
8e0912cbf1 Only prevent the unpublish or delete of a related item when configured to do so if it is related as a child, not as a parent (#18886)
* Only prevent the unpubkish or delete of a related item when configured to do so if it is related as a child, not as a parent.

* Fixed incorect parameter names.

* Fixed failing integration tests.

* Use using variable instead to reduce nesting

* Applied suggestions from code review.

* Used simple using statement throughout RelationService for consistency.

* Applied XML header comments consistently.

---------

Co-authored-by: mole <nikolajlauridsen@protonmail.ch>
2025-04-01 15:49:49 +02:00
Jacob OvergaardandGitHub bf89eae07f V15: Block package should not import from itself (#18902)
* fix: avoids circular dependencies by realising the 'block' package was importing from itself

* chore: lowers requirement to 9 after fixes

* chore: removes unused file
2025-04-01 12:02:35 +00:00
Jacob OvergaardandGitHub 19e873c7ad V15: File upload should use better Image & SVG preview (#18900)
* feat: uses `<uui-card-media />` to preview svg's

* fix: svg preview should support potentially very large images

* fix: adds alt attribute

* fix: adds `<uui-card-media />` for image previews as well

* fix: ensures all previews have at least a "title" attribute
2025-04-01 11:13:41 +00:00
Jacob OvergaardandGitHub d4acd53fd5 fix: fixes an issue where the serverUrl was shown twice and changes the component to use <uui-card-media /> instead (#18899) 2025-04-01 11:36:27 +01:00
Sebastiaan Janssen 85fc189ee8 Merge branch 'v15/dev' into contrib 2025-04-01 12:27:28 +02:00
Jeavon LeopoldandSebastiaan Janssen a48a70595a Check for NRT directory when populating LuceneIndexFolder 2025-04-01 11:57:40 +02:00
Sebastiaan Janssen 0f539088fa Amend PR 18741 2025-04-01 11:52:32 +02:00
Warren BuckleyandGitHub 80233a7850 Add in a VSCode .code-worskpace file (#18741) 2025-04-01 11:50:55 +02:00
Markus JohanssonandGitHub 1dd10eabd4 Use StringComparison.Ordinal in hot paths (#18893) 2025-04-01 11:39:22 +02:00
Warren BuckleyandGitHub e7a52de097 Updates telemetry dashboard UI (#18743)
* Update temeltry dashboard UI

* Uses headline prop/attr to set the header
* Uses a normal h3 rather than a h2 with H3 uui css class
* Updates the UUI slider to not show the value of 0,1,2 as not that useful to see when changing the slider

* Updates translation as it had a weirdly places br mid sentance
2025-04-01 11:29:57 +02:00
Jacob Overgaard 40a2d75282 Merge branch 'release/15.3.1' into v15/dev 2025-04-01 10:34:15 +02:00
Sebastiaan Janssen 7fb9df9b19 Merge branch 'v15/dev' into contrib 2025-04-01 10:15:54 +02:00
Andy ButlandandJacob Overgaard 1720692d3d Ensures date comparisons in schedule integration tests are made only on the datetime part to the second (#18894)
* Ensures date comparisons in schedule integration tests are made only on the date part.

* Include time part to the second.

* Ensure Kind is retained when truncating a date.

* Retain Kind for all truncation levels.
2025-04-01 09:45:35 +02:00
Andy ButlandandGitHub b798925ddb Adds webp to mime type map to support preview on upload file property editor. (#18863) 2025-04-01 09:18:31 +02:00
dependabot[bot]andJacob Overgaard f4caabaa98 Bump vite from 6.2.3 to 6.2.4 in /src/Umbraco.Web.UI.Login
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 6.2.3 to 6.2.4.
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/v6.2.4/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v6.2.4/packages/vite)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-04-01 09:17:56 +02:00
Andy ButlandandGitHub b1dbd80a07 Adds allow-same-origin to preview IFRAME. (#18895) 2025-04-01 09:07:08 +02:00
Andy ButlandandGitHub 882a0427e4 Ensures date comparisons in schedule integration tests are made only on the datetime part to the second (#18894)
* Ensures date comparisons in schedule integration tests are made only on the date part.

* Include time part to the second.

* Ensure Kind is retained when truncating a date.

* Retain Kind for all truncation levels.
2025-04-01 09:06:24 +02:00
d978a10b61 Fix: #18707 (#18859)
* observe condition changes

# Conflicts:
#	src/Umbraco.Web.UI.Client/src/apps/backoffice/backoffice.context.ts

* enable extension to be updated by late appended conditions

* update route when routes change

* remove commented code

* Update src/Umbraco.Web.UI.Client/src/libs/extension-api/registry/extension.registry.test.ts

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

* Revert "Update src/Umbraco.Web.UI.Client/src/libs/extension-api/registry/extension.registry.test.ts"

This reverts commit b8d1dd793d.

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-03-31 20:41:36 +02:00
Niels LyngsøandGitHub 7200b7b623 Bugfix: #18473 (#18765)
* add more icons

* add group id for inspection

* make outline style to make it not look disabled

* ensure that inherited has entries

* ensure current route is updated accordingly

* data marks

* shared across cultures tag

* fix sidebar group headline size
2025-03-31 14:18:47 +00:00
Andy ButlandandGitHub f200da48d8 Revert rather than prevent updates to sensitive properties on members without sensitive data access (#18794)
* Revert rather than prevent updates to sensitive properties on members without sensitive data access.

* Added suppression for integration test updates.
2025-03-31 15:16:41 +02:00
Jacob Overgaard fc815db80b bump version to 15.3.1 2025-03-31 14:48:54 +02:00
c5a3778ce7 Fix issue text overflow when user name is too long (#18587)
* Fix issue text overflow when user name is too long

* add ellipsis at the end of the text

* Bumped version of test helper

* Fixed document type design tab tests due to test helper changes

* Amends based on Niels' feedback

+ other code formatting tweaks.

* chore: remove unknown attribute on uui-tag

* fix: adds fallback text and uses `<umb-localize />` where applicable

---------

Co-authored-by: Lan Nguyen Thuy <lnt@umbraco.dk>
Co-authored-by: Nhu Dinh <hnd@umbraco.dk>
Co-authored-by: leekelleher <leekelleher@gmail.com>
Co-authored-by: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com>
2025-03-31 12:41:32 +00:00
Jacob Overgaard 8c1dfe5431 chore: fix lint errors 2025-03-31 14:07:32 +02:00
Jacob Overgaard 4cd43b264d fix: adds localization for "Unnamed" 2025-03-31 14:06:45 +02:00
Niels LyngsøandGitHub 3e835cd949 set circular dependency limit to 9 (#18786) 2025-03-31 13:55:26 +02:00
88b1b9de13 V15: bump @umbraco-ui/uui from 1.12.2 to 1.13.0
Co-authored-by: Lan Nguyen Thuy <lnt@umbraco.dk>
2025-03-31 13:50:23 +02:00
98e0615338 V15: Revert "Fix: RTE markup props not up to date issue" (#18879)
* Revert "simplifying the use of props (#18430)"

This reverts commit 347e898190.

* do not set value if identical check

cherry-picked from c03a8afab5

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

---------

Co-authored-by: Niels Lyngsø <nsl@umbraco.dk>
2025-03-31 13:13:00 +02:00
Nhu DinhandGitHub e18cdab185 V15 QA Added acceptance tests for inline editing mode (#18874)
* Added tests for block list with inline editing mode

* Added tests to create a document type witha block with inline editing mode

* Added tests for block grid inline editing mode

* Bumped version

* Changed npm commands

* Reverted npm command
2025-03-31 09:33:17 +00:00
26907f202f hotfix: context provider should not destroy instance (#18864)
* do not destroy instance

* Update src/Umbraco.Web.UI.Client/src/libs/context-api/provide/context-provider.ts

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

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-03-31 10:51:45 +02:00
Jacob OvergaardandGitHub 13d95f31f2 test: removes two tests that fails because of a non-fixated runtime (#18876)
i.e. the tests depend on a specific time interval, but the test runner does not guarantee that, and the tests are not strictly necessary anyway
2025-03-31 08:14:50 +00:00
Niels LyngsøandGitHub c422c82f21 await setup method (#18831) 2025-03-31 10:09:10 +02:00
Niels LyngsøandGitHub ae87f9ccd4 Implement context base for a few last contexts (#18865)
* do not destroy instance

* UmbDashboardHealthCheckElement

* correct last contexts to use context-base

* informing user that Umb.Condition.MenuAlias does not work

* parse host to Section Context
2025-03-31 10:02:25 +02:00
Niels LyngsøandGitHub 3359562e32 make the deprecation discoverable from TSC (#18856) 2025-03-31 09:56:42 +02:00
0e3c8dab8f Unpublish Document: Align UX of referenced items with trash and delete (#18860)
* Added member reference type model.

* Updated client-side types and sdk.

* Render member relations on the member info view.

* Add relation type for related member with migration.

* Extend tests for track relations to include member relations.

* Extend tests for relation repository.

* Extend tests for relation service.

* Addressed comments from Copilot review.

* Add relation notification to member deletion.

* Removed unused import.

* Updates from code review.

* make ref element globally available

* align naming

* use new reference list

* add interfaces for config

* export const

* Fixed failing integration tests.

* apply interface

* deprecate interface with wrong name

* fix import

* disable unpublish button when item or descendants are referenced

---------

Co-authored-by: Andy Butland <abutland73@gmail.com>
2025-03-31 09:52:46 +02:00
Lan Nguyen ThuyandJacob Overgaard 4d8406bb64 fix fix select all checkbox in scheduled publishing 2025-03-31 09:24:47 +02:00
dependabot[bot]andJacob Overgaard 2bc549bbc4 Bump vite from 6.2.2 to 6.2.3 in /src/Umbraco.Web.UI.Client
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 6.2.2 to 6.2.3.
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/v6.2.3/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v6.2.3/packages/vite)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-03-31 09:19:08 +02:00
Mads RasmussenandGitHub e8b7d9daee use uui-dialog-layout for all save + publishing dialogs (#18871) 2025-03-31 09:08:07 +02:00
Niels Lyngsø 48ac4b546e enable changing data-path on an existing context 2025-03-28 20:27:26 +01:00
Niels Lyngsø eb5c57fb94 do not destroy instance 2025-03-28 19:45:44 +01:00
468ca9ae94 Member relation tracking (#18852)
* Added member reference type model.

* Updated client-side types and sdk.

* Render member relations on the member info view.

* Add relation type for related member with migration.

* Extend tests for track relations to include member relations.

* Extend tests for relation repository.

* Extend tests for relation service.

* Addressed comments from Copilot review.

* Add relation notification to member deletion.

* Removed unused import.

* Updates from code review.

* export const

* Fixed failing integration tests.

* deprecate interface with wrong name

* fix import

---------

Co-authored-by: Mads Rasmussen <madsr@hey.com>
2025-03-28 15:34:25 +00:00
Niels Lyngsø 95cd6797cc Merge branch 'contrib' into v15/dev 2025-03-28 12:57:55 +01:00
Niels LyngsøandGitHub 6f63837dbc make load more 100% wide (#18821) 2025-03-28 11:49:07 +01:00
Andy ButlandandGitHub 6dc4eca607 Fixes null reference exception triggered when configuring to hide disabled users in the backoffice (#18823)
* Fixes null reference exception triggered when configuring to hide disabled users in the backoffice.

* Updated integration test suppressions.
2025-03-28 11:24:52 +01:00
Andy Butland aca908c549 Adds MemberTwoFactorLoginService. (#18810) 2025-03-28 11:20:38 +01:00
Andy ButlandandGitHub 685a05827e Adds MemberTwoFactorLoginService. (#18810) 2025-03-28 11:16:03 +01:00
Mads RasmussenandGitHub 07080604b9 Render Property "vary by segment"-toggle (#18813)
* render toggle for vary by segment

* use property-layout for appearance

* simplify labels

* remove duplicates

* render tag for vary by segment
2025-03-28 08:49:38 +01:00
Lee KelleherandGitHub 85176d1bf6 Fixes localization culture case-sensitive check (#18849)
Fixes #18801.
2025-03-27 15:24:45 +00:00
Jacob OvergaardandGitHub 52bf6bc412 feat: disables the internal dropzone if multiple=false and an upload is in progress (#18847)
you need to clear the files/queue before trying to upload something else, unless multiple=true
2025-03-27 14:20:36 +00:00
Lee KelleherandGitHub 47e09efec6 Fixes Dropdown property-editor validation (#18845) 2025-03-27 12:39:09 +00:00
9761ef899b Populate parent key on move and copy notifications (#18837)
* Populate parent key on move and copy notifications.

* Remove forgotten fixme

---------

Co-authored-by: nikolajlauridsen <nikolajlauridsen@protonmail.ch>
2025-03-27 10:33:01 +00:00
Jacob OvergaardandGitHub 164868f32a V15: Improve the dropzone for Upload Field (#18840)
* feat: maps up the CANCELLED status

* feat: uses the new dropzone input to render the dropzone

* feat: adds support for differing server urls

* chore: avoids a breaking change by storing the temporary file
2025-03-27 10:22:03 +00:00
Jacob OvergaardandGitHub a71ebe1902 V15: Improve the dropzone for Image Cropper (#18838)
* feat: uses the umb-dropzone-input to render the dropzone

* feat: loads in the blob url rather than reading the file into memory AND appends the server url

* chore: lit 3 compat

* feat: uses the umb-dropzone-input to render the dropzone

* Revert "feat: uses the umb-dropzone-input to render the dropzone"

This reverts commit bc1a6ae7df.

* feat: creates an object url directly from the File rather than the Blob

* feat: revokes the file data url from object storage

* feat: revokes object url on disconnect
2025-03-27 10:11:37 +00:00
Nhu DinhandGitHub 94f0add4d9 V15 Added acceptance tests for data type default configuration (#18740)
* Added tests for Approved Color default configuration

* Updated tests for Approved Color configuration

* Added tests for ChecklboxList configuration

* Added tests for Data type default configuration - part 1

* Added tests for data type configuration and updated tests due to test helper changes

* Added more steps to verify the default configuration

* Added tests for the default configuration and refactoring code

* Added steps to verify the TinyMCE default configuration

* Bumped version

* Fixed tests due to test helper changes

* Make all Data Type tests run in the pipeline

* Updated assertion steps

* Fixed format

* Bumped version

* Bumped version

* Comment failing tests

* Reverted npm command
2025-03-27 03:42:13 +00:00
Niels LyngsøandGitHub 9c88326681 lint corrections (#18833) 2025-03-26 19:36:17 +01:00
95e89f8481 Avoid a hash key generation and lookup when inserting in the LockingMechanism (#18243)
* Avoid a hash key generation and lookup when inserting in the LockingMechanism

* Added comments for CollectionsMarshal.GetValueRefOrAddDefault

* Added further comments and tests.

---------

Co-authored-by: Andy Butland <abutland73@gmail.com>
2025-03-26 16:55:03 +01:00
b25bcdc436 Fix bug uploading an image via the Media Picker is no longer automatically selected (#18667)
* Fix bug uploading an image via the Media Picker is no longer automatically selected

* Update src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/media-picker-modal.element.ts

Co-authored-by: Bjarne Fyrstenborg <bjarne_fyrstenborg@hotmail.com>

* Update src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/media-picker-modal.element.ts

Co-authored-by: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com>

* remove submit modal

* fix: avoids overriding the dropzone manager as that does not work with inheritance

* fix: set disabling of folders correctly

---------

Co-authored-by: Lan Nguyen Thuy <lnt@umbraco.dk>
Co-authored-by: Bjarne Fyrstenborg <bjarne_fyrstenborg@hotmail.com>
Co-authored-by: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com>
2025-03-26 15:04:19 +00:00
028da4545e Reduce CPU time when initiating RepositoryCacheKeys (#18267)
* Avoid an unneeded lookups in the Keys dictionary when initiating key cache

* Add further comments and unit tests around updated code.

---------

Co-authored-by: Andy Butland <abutland73@gmail.com>
2025-03-26 16:01:53 +01:00
Kenn JacobsenandGitHub 37035e6e7f Clean up leftover block item data when changing element variance (#18804) 2025-03-26 11:27:23 +01:00
Nhu DinhandGitHub f8c35bb7e9 V15 QA Added acceptance tests for user group default configuration (#18800)
* Updated userGroup tests due to test helper changes

* Added tests for user group default configuration

* Bumped version

* Fixed due to test helper changes

* Reverted npm command
2025-03-26 09:55:22 +00:00
Mads RasmussenandGitHub 056434fa27 fix preset logic (#18815) 2025-03-26 10:32:57 +01:00
Kenn JacobsenandGitHub e32eb06ddf Obsolete a few unused things. (#18814) 2025-03-26 10:24:09 +01:00
Niels LyngsøandGitHub 76dd141d86 implement validation context (#18808) 2025-03-26 10:12:25 +01:00
dependabot[bot]andJacob Overgaard 54601d42e2 Bump vite from 6.2.2 to 6.2.3 in /src/Umbraco.Web.UI.Login
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 6.2.2 to 6.2.3.
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/v6.2.3/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v6.2.3/packages/vite)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-03-26 09:25:04 +01:00
Andy Butland 619e025e7a Fixed issue where siblings of type at route are omitted from the result. (#18796) 2025-03-26 08:00:24 +01:00
Andy ButlandandGitHub 609e9f43e1 Fixed issue where siblings of type at route are omitted from the result. (#18796) 2025-03-26 07:37:04 +01:00
Mathias HelsengrenandGitHub ccc70fc786 Changes for the debug tag that makes it look better in dark mode (#18791) 2025-03-25 17:28:53 +01:00
Mathias HelsengrenandGitHub b5bffdfada Adjusting some of the colors for the dark theme so the ui looks a bit better when using dark theme (#18792) 2025-03-25 17:28:24 +01:00
Niels LyngsøandGitHub 0dd4443b75 Feature: validation synchronization as opt in (#18798)
* allow for this word

* getMessages

* split inherit and sync method

* sync feature

* rename sync report

* auto report + impl

* remove log

* double inheritance test

* one more test
2025-03-25 15:48:07 +01:00
Jacob OvergaardandGitHub 10f37494b6 V15: umb-dropzone extends umb-input-dropzone (#18784)
* feat: umb-dropzone should extend umb-input-dropzone

moves the umb-dropzone back into the media package and extends the umb-input-dropzone for common logic

* feat: adds a UmbDropzoneMediaManager class to handle media specifically

* chore: sort imports

* feat: adds a browse() method

* removes unused export

* use correct import

* fix: document and media type import should use other dropzone

* docs(storybook): remove old argument

* feat: adds umb-dropzone-media element and deprecates umb-dropzone element

* feat: use umb-dropzone-media instead

* adds export for dropzone

* feat: adds a slot to show an additional text above the dropzone graphics

* feat: the dropzone should fill out its host component

* feat: adds back a text to describe where to drop files

* remove unused import

* fix: removes overflow to allow full border
2025-03-25 14:09:41 +00:00
Kenn JacobsenandGitHub 2711ac07ac Only validate invariant properties when strictly necessary (#18729) 2025-03-25 12:58:01 +01:00
Lee KelleherandGitHub 80e092069b Tiptap RTE: Statusbar extension type (#18789) 2025-03-25 12:52:27 +01:00
Sebastiaan Janssen c23e2eb6f2 Merge branch 'v15/dev' into contrib 2025-03-25 10:44:16 +01:00
Mads RasmussenandJacob Overgaard 25837e66fc fix dropzone circular 2025-03-24 16:35:41 +01:00
Mads Rasmussen 3be766fe2a fix build 2025-03-24 16:04:44 +01:00
Mads RasmussenandGitHub 34649db5c9 Block Grid: Fix circular dependencies (#18782)
* Update input-rich-media.element.ts

* fix circular

* fixes circular dependencies in the validation module

* Update bind-to-validation.lit-directive.ts

* fix workspace circular

* Update error-viewer-modal.token.ts

* move contexts next to component

* fix import errors

* import local components from one file

* lint

* import in property editor file

* fix import

* remove duplicate import
2025-03-24 14:46:34 +00:00
4c679a5f4c Update input-rich-media.element.ts (#18772)
Co-authored-by: Niels Lyngsø <niels.lyngso@gmail.com>
2025-03-24 14:10:13 +00:00
36c66177fc Feature: Tree expansion state (#18227)
* implement tree expansion logic

* wip test example

* support complex expansion

* extend entity

* extend with model

* Update tree-item-context.interface.ts

* use expansion model to observe open state

* clean up

* fall back to tree context

* Update default-tree.context.ts

* Update default-tree.context.ts

* Update default-tree.context.ts

* clean up

* simplify model and state

* refactor to manager

* remove test data

* Update default-tree.context.ts

* rename

* add get method

* rename to collapse

* all collapse all method

* fix collapse logic

* add js docs

* add tests for expansion manager

* do not load children if the item is already open

* Update tree-item-element-base.ts

* config to expand tree root in pickers

* expand tree root for duplicate to

* Update tree-expansion-manager.test.ts

* make methods async

* use array state

* add isExpanded helper

* refactor to use isExpanded helper

* fix type issues

---------

Co-authored-by: Niels Lyngsø <niels.lyngso@gmail.com>
Co-authored-by: Niels Lyngsø <nsl@umbraco.dk>
2025-03-24 14:49:09 +01:00
Mads RasmussenandGitHub ad443c7b13 fix circular (#18773) 2025-03-24 14:47:04 +01:00
Mads RasmussenandGitHub 0b6fe91fd3 Validation: Fixes circular dependencies in the validation module (#18774)
* fixes circular dependencies in the validation module

* Update bind-to-validation.lit-directive.ts
2025-03-24 14:39:18 +01:00
Mads RasmussenandGitHub ee496d2730 fix workspace circular (#18775) 2025-03-24 14:32:00 +01:00
Mads RasmussenandGitHub 384b9bdb65 Update error-viewer-modal.token.ts (#18776) 2025-03-24 14:31:40 +01:00
Jacob OvergaardandGitHub 63113c4552 V15: New dropzone component available for the Backoffice (#18753)
* create a symlink between local Client .vscode snippets and global snippets for ease of use

* fix: no need to specify `Element` in the snippet as that is pulled from the filename

Because of our convention with `x.element.ts` you would have ended up with `UmbXElementElement`

* feat: adds new component `umb-input-dropzone`

* docs(storybook): more stories

* feat: construct the temporary files centrally along with an `AbortController` and use its signal

* feat: makes UmbInputDropzone form aware

* feat: introduces a change event

* chore: temporary changes before changing upload field

* feat: adds default slot

* docs: adds jsdocs

* feat: adds more properties

* feat: adds dashed styling

* feat: adds multiple support

* feat: allows to cancel file

* feat: separate **cancel** and **remove**

* fix stylibg

* move dropzone element

* move input-dropzone into dropzone package

* feat: introduces a 'dropzone' package

* import for backward compatibility

* remove ambigious export

* reexport everything from dropzone

* fix import

* cleanup test files

* use correct import paths

* test: make sure folder exists before writing to it

* adds export for modals

* adds entrypoint for dropzone package

* use the AbortController directly on the temporary file object

* uses correct icon name

* feat: adds ability to remove all files and cancel the request

* feat: adds styling for the uploader

and enables it to work in multiple mode with classes over id's

* do not let the content exceed its boundaries

* feat: formats progress with 2 decimals

* feat: formats with 0 decimals

* fix: returns cancel error

* fix: maps cancel errors back to the uploadable item

* fix: do not proceed with media items if the request was cancelled

* chore: mark exports from media <- dropzone as deprecated

* fix: use correct attribute and remove a todo with localizations

* fix: use correct attribute and remove a todo with localizations

* fix: allow to specify parent through attribute

* feat: align attribute `disableFolderUpload` between dropzone components
2025-03-24 13:17:48 +00:00
Sven GeusensandGitHub 39cad5b2ea Add variancy information to reference response model (#18645)
* Made variant info available on DocumentReferenceResponseModel

* Fix scope issue

* PR Feedback + correct scoping
2025-03-24 12:10:30 +01:00
Andy ButlandandGitHub 30633fe728 Only validate for duplicate member email address when configured to do so (#18747)
* Only validate for duplicate member email address when configured to do so.

* Lookup member after creation by user name rather than email, as only the former is guaranteed to be unique.
2025-03-24 12:02:50 +01:00
Andy ButlandandGitHub 394210a8f7 Uses correct preview cookie same site and secure settings to allow preview mode to flow between links in the preview frame (#18640)
* Uses correct preview cookie same site and secure settings to allow preview mode to flow between links in the preview frame.

* Fixed comment.
2025-03-21 15:20:08 +01:00
Niels LyngsøandGitHub 45b0e43b89 Feature: validation variant filtering + up-front validation path translation (#18561)
* implement console.warn

* variant specific validation contexts

* getByType method

* test for getByType

* corrected test

* no need for fallback here

* revert

* support path starting with array query

* draft

* scoped json path mapper

* JSDocs correction

* rename function

* umbQueryMapperForJsonPaths

* JSDocs

* Validation Path Translation manager

* Validation Property Path Translation Manager

* rename

* rename folder

* rename folder correction

* impl for server validation context

* rename

* .

* clean up

* re introduce queryConstructor

* correct mappers

* correct imports

* implement path translator

* content detail path translator

* test with extension type propertyValidationPathTranslator

* implement translator system for block editors

* validation fixes to prevent issues
2025-03-21 12:59:06 +01:00
Niels LyngsøandGitHub c21da38c32 rename icon and regenerate icons (#18754) 2025-03-21 11:36:46 +01:00
Niels LyngsøandGitHub ef751ca699 style adjustments (#18757) 2025-03-21 11:35:37 +01:00
Niels LyngsøandGitHub 52debc3010 make numbers go right side (#18759) 2025-03-21 11:34:36 +01:00
Niels LyngsøandGitHub a38a9b29a3 addd var(--umb-card-medium-min-width) (#18760) 2025-03-21 11:34:03 +01:00
Jacob Overgaard e346ac2bee build: add emptyOutDir to base vite config to ensure modules are emptied before build
this does not have any effect on the release build since it always starts from fresh - it is mostly to ensure no dev files are left behind locally
2025-03-21 11:05:57 +01:00
Andreas ZerbstandGitHub 425901636e V15 QA updated nightly pipeline tests (#18746)
* Removed tests for removed features

* Fixed failing tests

* Bumped version
2025-03-21 08:03:28 +00:00
Niels Lyngsø d9f17b36c6 Merge branch 'release/15.3' into v15/dev
# Conflicts:
#	src/Umbraco.Web.UI.Client/package-lock.json
#	src/Umbraco.Web.UI.Client/package.json
#	src/Umbraco.Web.UI.Client/src/packages/multi-url-picker/property-editor/property-editor-ui-multi-url-picker.element.ts
#	src/Umbraco.Web.UI.Client/src/packages/property-editors/multiple-text-string/property-editor-ui-multiple-text-string.element.ts
#	version.json
2025-03-20 18:53:11 +01:00
Lee KelleherandGitHub 5fe5a16158 Tiptap RTE: Table Properties toolbar menu + modal (#18751)
* Tiptap Table Properties modal

* Attempting to reduce the cyclomatic complexity
2025-03-20 17:36:28 +01:00
131c9cda6f hotfix #18735 (#18750)
* fix multiple text string validation

* notify about messages

* cherry picked fix

* protection again unnecessary calls

* json path cherry pick + tests

* validation message change lock

* cherry pick from control lifecycle

* optimization

* propagate errors

* cherry picked sync

* query umb-input-multiple-text-string

* remove unused import

* remove optional chain expression

* use !

* outcomment the error handling

* outcomment more promise rejection error

* Fixed issue with multi URL picker.

* remove unesecary warning

---------

Co-authored-by: Andy Butland <abutland73@gmail.com>
2025-03-20 16:22:05 +00:00
Niels LyngsøandGitHub e999d3977c change button look (#18678) 2025-03-20 12:55:29 +01:00
Sven Geusens f7854b8c95 Version bump 2025-03-20 11:08:38 +01:00
831 changed files with 22063 additions and 7745 deletions
+1
View File
@@ -0,0 +1 @@
../src/Umbraco.Web.UI.Client/.vscode/lit.code-snippets
+5
View File
@@ -0,0 +1,5 @@
{
"cSpell.words": [
"unprovide"
]
}
+3 -3
View File
@@ -45,8 +45,8 @@
<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="Examine" Version="3.7.1" />
<PackageVersion Include="Examine.Core" Version="3.7.1" />
<PackageVersion Include="HtmlAgilityPack" Version="1.11.74" />
<PackageVersion Include="JsonPatch.Net" Version="3.1.1" />
<PackageVersion Include="K4os.Compression.LZ4" Version="1.3.8" />
@@ -100,4 +100,4 @@
<!-- Examine.Lucene brings in a vulnerable version of Lucene.Net.Replicator -->
<PackageVersion Include="Lucene.Net.Replicator" Version="4.8.0-beta00017" />
</ItemGroup>
</Project>
</Project>
+10
View File
@@ -0,0 +1,10 @@
{
"folders": [
{
"path": "src/Umbraco.Web.UI.Client"
},
{
"path": "src/Umbraco.Web.UI.Login"
}
]
}
+4
View File
@@ -829,6 +829,8 @@ stages:
condition: and(succeeded(), or(eq(dependencies.Build.outputs['A.build.NBGV_PublicRelease'], 'True'), ${{parameters.myGetDeploy}}))
jobs:
- job:
pool:
vmImage: "windows-latest" # NuGetCommand@2 is no longer supported on Ubuntu 24.04 so we'll use windows until an alternative is available.
displayName: Push to pre-release feed
steps:
- checkout: none
@@ -890,6 +892,8 @@ stages:
condition: and(succeeded(), or(eq(dependencies.Build.outputs['A.build.NBGV_PublicRelease'], 'True'), ${{parameters.nuGetDeploy}}))
jobs:
- job:
pool:
vmImage: "windows-latest" # NuGetCommand@2 is no longer supported on Ubuntu 24.04 so we'll use windows until an alternative is available.
displayName: Push to NuGet
steps:
- checkout: none
@@ -48,7 +48,7 @@ public class ConfigureUmbracoMemberAuthenticationDeliveryApiSwaggerGenOptions :
Id = AuthSchemeName,
}
},
new string[] { }
[]
}
}
};
@@ -185,7 +185,7 @@ internal sealed class ApiMediaQueryService : IApiMediaQueryService
}
private Attempt<PagedModel<Guid>, ApiMediaQueryOperationStatus> PagedResult(IEnumerable<IPublishedContent> children, int skip, int take)
private static Attempt<PagedModel<Guid>, ApiMediaQueryOperationStatus> PagedResult(IEnumerable<IPublishedContent> children, int skip, int take)
{
IPublishedContent[] childrenAsArray = children as IPublishedContent[] ?? children.ToArray();
var result = new PagedModel<Guid>
@@ -0,0 +1,46 @@
using Asp.Versioning;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Api.Common.ViewModels.Pagination;
using Umbraco.Cms.Api.Management.Factories;
using Umbraco.Cms.Api.Management.ViewModels.TrackedReferences;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Services;
namespace Umbraco.Cms.Api.Management.Controllers.DataType.References;
[ApiVersion("1.0")]
public class ReferencedByDataTypeController : DataTypeControllerBase
{
private readonly IDataTypeService _dataTypeService;
private readonly IRelationTypePresentationFactory _relationTypePresentationFactory;
public ReferencedByDataTypeController(IDataTypeService dataTypeService, IRelationTypePresentationFactory relationTypePresentationFactory)
{
_dataTypeService = dataTypeService;
_relationTypePresentationFactory = relationTypePresentationFactory;
}
/// <summary>
/// Gets a paged list of references for the current data type, so you can see where it is being used.
/// </summary>
[HttpGet("{id:guid}/referenced-by")]
[MapToApiVersion("1.0")]
[ProducesResponseType(typeof(PagedViewModel<IReferenceResponseModel>), StatusCodes.Status200OK)]
public async Task<ActionResult<PagedViewModel<IReferenceResponseModel>>> ReferencedBy(
CancellationToken cancellationToken,
Guid id,
int skip = 0,
int take = 20)
{
PagedModel<RelationItemModel> relationItems = await _dataTypeService.GetPagedRelationsAsync(id, skip, take);
var pagedViewModel = new PagedViewModel<IReferenceResponseModel>
{
Total = relationItems.Total,
Items = await _relationTypePresentationFactory.CreateReferenceResponseModelsAsync(relationItems.Items),
};
return pagedViewModel;
}
}
@@ -1,4 +1,4 @@
using Asp.Versioning;
using Asp.Versioning;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Api.Management.Factories;
@@ -10,6 +10,7 @@ using Umbraco.Cms.Core.Services.OperationStatus;
namespace Umbraco.Cms.Api.Management.Controllers.DataType;
[ApiVersion("1.0")]
[Obsolete("Please use ReferencedByDataTypeController and the referenced-by endpoint. Scheduled for removal in Umbraco 17.")]
public class ReferencesDataTypeController : DataTypeControllerBase
{
private readonly IDataTypeService _dataTypeService;
@@ -1,4 +1,4 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Api.Management.Controllers.Content;
@@ -140,6 +140,10 @@ public abstract class DocumentControllerBase : ContentControllerBase
.WithDetail(
"An unspecified error occurred while (un)publishing. Please check the logs for additional information.")
.Build()),
ContentPublishingOperationStatus.TaskResultNotFound => NotFound(problemDetailsBuilder
.WithTitle("The result of the submitted task could not be found")
.Build()),
_ => StatusCode(StatusCodes.Status500InternalServerError, "Unknown content operation status."),
});
@@ -35,7 +35,7 @@ public class PublishDocumentWithDescendantsController : DocumentControllerBase
[HttpPut("{id:guid}/publish-with-descendants")]
[MapToApiVersion("1.0")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(typeof(PublishWithDescendantsResultModel), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
public async Task<IActionResult> PublishWithDescendants(CancellationToken cancellationToken, Guid id, PublishDocumentWithDescendantsRequestModel requestModel)
@@ -54,10 +54,15 @@ public class PublishDocumentWithDescendantsController : DocumentControllerBase
id,
requestModel.Cultures,
BuildPublishBranchFilter(requestModel),
CurrentUserKey(_backOfficeSecurityAccessor));
CurrentUserKey(_backOfficeSecurityAccessor),
true);
return attempt.Success
? Ok()
return attempt.Success && attempt.Result.AcceptedTaskId.HasValue
? Ok(new PublishWithDescendantsResultModel
{
TaskId = attempt.Result.AcceptedTaskId.Value,
IsComplete = false
})
: DocumentPublishingOperationStatusResult(attempt.Status, failedBranchItems: attempt.Result.FailedItems);
}
@@ -0,0 +1,69 @@
using Asp.Versioning;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Api.Management.ViewModels.Document;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Actions;
using Umbraco.Cms.Core.Models.ContentPublishing;
using Umbraco.Cms.Core.Security.Authorization;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Services.OperationStatus;
using Umbraco.Cms.Web.Common.Authorization;
using Umbraco.Extensions;
namespace Umbraco.Cms.Api.Management.Controllers.Document;
[ApiVersion("1.0")]
public class PublishDocumentWithDescendantsResultController : DocumentControllerBase
{
private readonly IAuthorizationService _authorizationService;
private readonly IContentPublishingService _contentPublishingService;
public PublishDocumentWithDescendantsResultController(
IAuthorizationService authorizationService,
IContentPublishingService contentPublishingService)
{
_authorizationService = authorizationService;
_contentPublishingService = contentPublishingService;
}
[HttpGet("{id:guid}/publish-with-descendants/result/{taskId:guid}")]
[MapToApiVersion("1.0")]
[ProducesResponseType(typeof(PublishWithDescendantsResultModel), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
public async Task<IActionResult> PublishWithDescendantsResult(CancellationToken cancellationToken, Guid id, Guid taskId)
{
AuthorizationResult authorizationResult = await _authorizationService.AuthorizeResourceAsync(
User,
ContentPermissionResource.Branch(ActionPublish.ActionLetter, id),
AuthorizationPolicies.ContentPermissionByResource);
if (!authorizationResult.Succeeded)
{
return Forbidden();
}
// Check if the publishing task has completed, if not, return the status.
var isPublishing = await _contentPublishingService.IsPublishingBranchAsync(taskId);
if (isPublishing)
{
return Ok(new PublishWithDescendantsResultModel
{
TaskId = taskId,
IsComplete = false
});
};
// If completed, get the result and return the status.
Attempt<ContentPublishingBranchResult, ContentPublishingOperationStatus> attempt = await _contentPublishingService.GetPublishBranchResultAsync(taskId);
return attempt.Success
? Ok(new PublishWithDescendantsResultModel
{
TaskId = taskId,
IsComplete = true
})
: DocumentPublishingOperationStatusResult(attempt.Status, failedBranchItems: attempt.Result.FailedItems);
}
}
@@ -0,0 +1,50 @@
using Asp.Versioning;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Api.Common.ViewModels.Pagination;
using Umbraco.Cms.Api.Management.Factories;
using Umbraco.Cms.Api.Management.ViewModels.TrackedReferences;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Services;
namespace Umbraco.Cms.Api.Management.Controllers.Document.RecycleBin;
[ApiVersion("1.0")]
public class ReferencedByDocumentRecycleBinController : DocumentRecycleBinControllerBase
{
private readonly ITrackedReferencesService _trackedReferencesService;
private readonly IRelationTypePresentationFactory _relationTypePresentationFactory;
public ReferencedByDocumentRecycleBinController(
IEntityService entityService,
IDocumentPresentationFactory documentPresentationFactory,
ITrackedReferencesService trackedReferencesService,
IRelationTypePresentationFactory relationTypePresentationFactory)
: base(entityService, documentPresentationFactory)
{
_trackedReferencesService = trackedReferencesService;
_relationTypePresentationFactory = relationTypePresentationFactory;
}
/// <summary>
/// Gets a paged list of tracked references for all items in the document recycle bin, so you can see where an item is being used.
/// </summary>
[HttpGet("referenced-by")]
[MapToApiVersion("1.0")]
[ProducesResponseType(typeof(PagedViewModel<IReferenceResponseModel>), StatusCodes.Status200OK)]
public async Task<ActionResult<PagedViewModel<IReferenceResponseModel>>> ReferencedBy(
CancellationToken cancellationToken,
int skip = 0,
int take = 20)
{
PagedModel<RelationItemModel> relationItems = await _trackedReferencesService.GetPagedRelationsForRecycleBinAsync(UmbracoObjectTypes.Document, skip, take, true);
var pagedViewModel = new PagedViewModel<IReferenceResponseModel>
{
Total = relationItems.Total,
Items = await _relationTypePresentationFactory.CreateReferenceResponseModelsAsync(relationItems.Items),
};
return pagedViewModel;
}
}
@@ -1,4 +1,4 @@
using Asp.Versioning;
using Asp.Versioning;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Api.Common.ViewModels.Pagination;
@@ -45,6 +45,6 @@ public class AreReferencedDocumentController : DocumentControllerBase
Items = _umbracoMapper.MapEnumerable<Guid, ReferenceByIdModel>(distinctByKeyItemsWithReferencedRelations.Items),
};
return await Task.FromResult(pagedViewModel);
return pagedViewModel;
}
}
@@ -45,6 +45,6 @@ public class ReferencedByDocumentController : DocumentControllerBase
Items = await _relationTypePresentationFactory.CreateReferenceResponseModelsAsync(relationItems.Items),
};
return await Task.FromResult(pagedViewModel);
return pagedViewModel;
}
}
@@ -1,4 +1,4 @@
using Asp.Versioning;
using Asp.Versioning;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Api.Common.ViewModels.Pagination;
@@ -45,6 +45,6 @@ public class ReferencedDescendantsDocumentController : DocumentControllerBase
Items = _umbracoMapper.MapEnumerable<RelationItemModel, ReferenceByIdModel>(relationItems.Items),
};
return await Task.FromResult(pagedViewModel);
return pagedViewModel;
}
}
@@ -4,6 +4,7 @@ using Umbraco.Cms.Api.Management.Controllers.Tree;
using Umbraco.Cms.Api.Management.Factories;
using Umbraco.Cms.Api.Management.Routing;
using Umbraco.Cms.Api.Management.Services.Entities;
using Umbraco.Cms.Api.Management.ViewModels;
using Umbraco.Cms.Api.Management.ViewModels.Tree;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Cache;
@@ -33,7 +34,7 @@ public abstract class DocumentTreeControllerBase : UserStartNodeTreeControllerBa
AppCaches appCaches,
IBackOfficeSecurityAccessor backofficeSecurityAccessor,
IDocumentPresentationFactory documentPresentationFactory)
: base(entityService, userStartNodeEntitiesService, dataTypeService)
: base(entityService, userStartNodeEntitiesService, dataTypeService)
{
_publicAccessService = publicAccessService;
_appCaches = appCaches;
@@ -52,6 +53,8 @@ public abstract class DocumentTreeControllerBase : UserStartNodeTreeControllerBa
if (entity is IDocumentEntitySlim documentEntitySlim)
{
responseModel.IsProtected = _publicAccessService.IsProtected(entity.Path);
responseModel.Ancestors = EntityService.GetPathKeys(entity, omitSelf: true)
.Select(x => new ReferenceByIdModel(x));
responseModel.IsTrashed = entity.Trashed;
responseModel.Id = entity.Key;
responseModel.CreateDate = entity.CreateDate;
@@ -1,11 +1,14 @@
using Asp.Versioning;
using Asp.Versioning;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.DependencyInjection;
using Umbraco.Cms.Api.Management.Factories;
using Umbraco.Cms.Api.Management.ViewModels.Document;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Core.Models.ContentEditing;
using Umbraco.Cms.Core.Security;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Services.OperationStatus;
@@ -16,15 +19,32 @@ public class ValidateCreateDocumentController : CreateDocumentControllerBase
{
private readonly IDocumentEditingPresentationFactory _documentEditingPresentationFactory;
private readonly IContentEditingService _contentEditingService;
private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor;
[Obsolete("Please use the constructor taking all parameters. Scheduled for removal in Umbraco 17.")]
public ValidateCreateDocumentController(
IAuthorizationService authorizationService,
IDocumentEditingPresentationFactory documentEditingPresentationFactory,
IContentEditingService contentEditingService)
: this(
authorizationService,
documentEditingPresentationFactory,
contentEditingService,
StaticServiceProvider.Instance.GetRequiredService<IBackOfficeSecurityAccessor>())
{
}
[ActivatorUtilitiesConstructor]
public ValidateCreateDocumentController(
IAuthorizationService authorizationService,
IDocumentEditingPresentationFactory documentEditingPresentationFactory,
IContentEditingService contentEditingService,
IBackOfficeSecurityAccessor backOfficeSecurityAccessor)
: base(authorizationService)
{
_documentEditingPresentationFactory = documentEditingPresentationFactory;
_contentEditingService = contentEditingService;
_backOfficeSecurityAccessor = backOfficeSecurityAccessor;
}
[HttpPost("validate")]
@@ -36,7 +56,10 @@ public class ValidateCreateDocumentController : CreateDocumentControllerBase
=> await HandleRequest(requestModel, async () =>
{
ContentCreateModel model = _documentEditingPresentationFactory.MapCreateModel(requestModel);
Attempt<ContentValidationResult, ContentEditingOperationStatus> result = await _contentEditingService.ValidateCreateAsync(model);
Attempt<ContentValidationResult, ContentEditingOperationStatus> result =
await _contentEditingService.ValidateCreateAsync(
model,
CurrentUserKey(_backOfficeSecurityAccessor));
return result.Success
? Ok()
@@ -1,11 +1,14 @@
using Asp.Versioning;
using Asp.Versioning;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.DependencyInjection;
using Umbraco.Cms.Api.Management.Factories;
using Umbraco.Cms.Api.Management.ViewModels.Document;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Core.Models.ContentEditing;
using Umbraco.Cms.Core.Security;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Services.OperationStatus;
@@ -17,15 +20,32 @@ public class ValidateUpdateDocumentController : UpdateDocumentControllerBase
{
private readonly IContentEditingService _contentEditingService;
private readonly IDocumentEditingPresentationFactory _documentEditingPresentationFactory;
private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor;
[Obsolete("Please use the constructor taking all parameters. Scheduled for removal in Umbraco 17.")]
public ValidateUpdateDocumentController(
IAuthorizationService authorizationService,
IContentEditingService contentEditingService,
IDocumentEditingPresentationFactory documentEditingPresentationFactory)
: this(
authorizationService,
contentEditingService,
documentEditingPresentationFactory,
StaticServiceProvider.Instance.GetRequiredService<IBackOfficeSecurityAccessor>())
{
}
[ActivatorUtilitiesConstructor]
public ValidateUpdateDocumentController(
IAuthorizationService authorizationService,
IContentEditingService contentEditingService,
IDocumentEditingPresentationFactory documentEditingPresentationFactory,
IBackOfficeSecurityAccessor backOfficeSecurityAccessor)
: base(authorizationService)
{
_contentEditingService = contentEditingService;
_documentEditingPresentationFactory = documentEditingPresentationFactory;
_backOfficeSecurityAccessor = backOfficeSecurityAccessor;
}
[HttpPut("{id:guid}/validate")]
@@ -62,7 +82,11 @@ public class ValidateUpdateDocumentController : UpdateDocumentControllerBase
=> await HandleRequest(id, requestModel, async () =>
{
ValidateContentUpdateModel model = _documentEditingPresentationFactory.MapValidateUpdateModel(requestModel);
Attempt<ContentValidationResult, ContentEditingOperationStatus> result = await _contentEditingService.ValidateUpdateAsync(id, model);
Attempt<ContentValidationResult, ContentEditingOperationStatus> result =
await _contentEditingService.ValidateUpdateAsync(
id,
model,
CurrentUserKey(_backOfficeSecurityAccessor));
return result.Success
? Ok()
@@ -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();
}
}
@@ -0,0 +1,50 @@
using Asp.Versioning;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Api.Common.ViewModels.Pagination;
using Umbraco.Cms.Api.Management.Factories;
using Umbraco.Cms.Api.Management.ViewModels.TrackedReferences;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Services;
namespace Umbraco.Cms.Api.Management.Controllers.Media.RecycleBin;
[ApiVersion("1.0")]
public class ReferencedByMediaRecycleBinController : MediaRecycleBinControllerBase
{
private readonly ITrackedReferencesService _trackedReferencesService;
private readonly IRelationTypePresentationFactory _relationTypePresentationFactory;
public ReferencedByMediaRecycleBinController(
IEntityService entityService,
IMediaPresentationFactory mediaPresentationFactory,
ITrackedReferencesService trackedReferencesService,
IRelationTypePresentationFactory relationTypePresentationFactory)
: base(entityService, mediaPresentationFactory)
{
_trackedReferencesService = trackedReferencesService;
_relationTypePresentationFactory = relationTypePresentationFactory;
}
/// <summary>
/// Gets a paged list of tracked references for all items in the media recycle bin, so you can see where an item is being used.
/// </summary>
[HttpGet("referenced-by")]
[MapToApiVersion("1.0")]
[ProducesResponseType(typeof(PagedViewModel<IReferenceResponseModel>), StatusCodes.Status200OK)]
public async Task<ActionResult<PagedViewModel<IReferenceResponseModel>>> ReferencedBy(
CancellationToken cancellationToken,
int skip = 0,
int take = 20)
{
PagedModel<RelationItemModel> relationItems = await _trackedReferencesService.GetPagedRelationsForRecycleBinAsync(UmbracoObjectTypes.Media, skip, take, true);
var pagedViewModel = new PagedViewModel<IReferenceResponseModel>
{
Total = relationItems.Total,
Items = await _relationTypePresentationFactory.CreateReferenceResponseModelsAsync(relationItems.Items),
};
return pagedViewModel;
}
}
@@ -1,4 +1,4 @@
using Asp.Versioning;
using Asp.Versioning;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Api.Common.ViewModels.Pagination;
@@ -46,6 +46,6 @@ public class AreReferencedMediaController : MediaControllerBase
Items = _umbracoMapper.MapEnumerable<Guid, ReferenceByIdModel>(distinctByKeyItemsWithReferencedRelations.Items),
};
return await Task.FromResult(pagedViewModel);
return pagedViewModel;
}
}
@@ -45,6 +45,6 @@ public class ReferencedByMediaController : MediaControllerBase
Items = await _relationTypePresentationFactory.CreateReferenceResponseModelsAsync(relationItems.Items),
};
return await Task.FromResult(pagedViewModel);
return pagedViewModel;
}
}
@@ -1,4 +1,4 @@
using Asp.Versioning;
using Asp.Versioning;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Api.Common.ViewModels.Pagination;
@@ -45,6 +45,6 @@ public class ReferencedDescendantsMediaController : MediaControllerBase
Items = _umbracoMapper.MapEnumerable<RelationItemModel, ReferenceByIdModel>(relationItems.Items),
};
return await Task.FromResult(pagedViewModel);
return pagedViewModel;
}
}
@@ -0,0 +1,51 @@
using Asp.Versioning;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Api.Common.ViewModels.Pagination;
using Umbraco.Cms.Api.Management.ViewModels;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Mapping;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Services;
namespace Umbraco.Cms.Api.Management.Controllers.Member.References;
[ApiVersion("1.0")]
public class AreReferencedMemberController : MemberControllerBase
{
private readonly ITrackedReferencesService _trackedReferencesSkipTakeService;
private readonly IUmbracoMapper _umbracoMapper;
public AreReferencedMemberController(ITrackedReferencesService trackedReferencesSkipTakeService, IUmbracoMapper umbracoMapper)
{
_trackedReferencesSkipTakeService = trackedReferencesSkipTakeService;
_umbracoMapper = umbracoMapper;
}
/// <summary>
/// Gets a page list of the items used in any kind of relation from selected keys.
/// </summary>
/// <remarks>
/// Used when bulk deleting content/media and bulk unpublishing content (delete and unpublish on List view).
/// This is basically finding children of relations.
/// </remarks>
// [HttpGet("item")]
[HttpGet("are-referenced")]
[MapToApiVersion("1.0")]
[ProducesResponseType(typeof(PagedViewModel<ReferenceByIdModel>), StatusCodes.Status200OK)]
public async Task<ActionResult<PagedViewModel<ReferenceByIdModel>>> GetPagedReferencedItems(
CancellationToken cancellationToken,
[FromQuery(Name="id")] HashSet<Guid> ids,
int skip = 0,
int take = 20)
{
PagedModel<Guid> distinctByKeyItemsWithReferencedRelations = await _trackedReferencesSkipTakeService.GetPagedKeysWithDependentReferencesAsync(ids, Constants.ObjectTypes.Member, skip, take);
var pagedViewModel = new PagedViewModel<ReferenceByIdModel>
{
Total = distinctByKeyItemsWithReferencedRelations.Total,
Items = _umbracoMapper.MapEnumerable<Guid, ReferenceByIdModel>(distinctByKeyItemsWithReferencedRelations.Items),
};
return pagedViewModel;
}
}
@@ -0,0 +1,50 @@
using Asp.Versioning;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Api.Common.ViewModels.Pagination;
using Umbraco.Cms.Api.Management.Factories;
using Umbraco.Cms.Api.Management.ViewModels.TrackedReferences;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Services;
namespace Umbraco.Cms.Api.Management.Controllers.Member.References;
[ApiVersion("1.0")]
public class ReferencedByMemberController : MemberControllerBase
{
private readonly ITrackedReferencesService _trackedReferencesService;
private readonly IRelationTypePresentationFactory _relationTypePresentationFactory;
public ReferencedByMemberController(ITrackedReferencesService trackedReferencesService, IRelationTypePresentationFactory relationTypePresentationFactory)
{
_trackedReferencesService = trackedReferencesService;
_relationTypePresentationFactory = relationTypePresentationFactory;
}
/// <summary>
/// Gets a page list of tracked references for the current item, so you can see where an item is being used.
/// </summary>
/// <remarks>
/// Used by info tabs on content, media etc. and for the delete and unpublish of single items.
/// This is basically finding parents of relations.
/// </remarks>
[HttpGet("{id:guid}/referenced-by")]
[MapToApiVersion("1.0")]
[ProducesResponseType(typeof(PagedViewModel<IReferenceResponseModel>), StatusCodes.Status200OK)]
public async Task<ActionResult<PagedViewModel<IReferenceResponseModel>>> ReferencedBy(
CancellationToken cancellationToken,
Guid id,
int skip = 0,
int take = 20)
{
PagedModel<RelationItemModel> relationItems = await _trackedReferencesService.GetPagedRelationsForItemAsync(id, skip, take, true);
var pagedViewModel = new PagedViewModel<IReferenceResponseModel>
{
Total = relationItems.Total,
Items = await _relationTypePresentationFactory.CreateReferenceResponseModelsAsync(relationItems.Items),
};
return pagedViewModel;
}
}
@@ -0,0 +1,50 @@
using Asp.Versioning;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Api.Common.ViewModels.Pagination;
using Umbraco.Cms.Api.Management.ViewModels;
using Umbraco.Cms.Core.Mapping;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Services;
namespace Umbraco.Cms.Api.Management.Controllers.Member.References;
[ApiVersion("1.0")]
public class ReferencedDescendantsMemberController : MemberControllerBase
{
private readonly ITrackedReferencesService _trackedReferencesSkipTakeService;
private readonly IUmbracoMapper _umbracoMapper;
public ReferencedDescendantsMemberController(ITrackedReferencesService trackedReferencesSkipTakeService, IUmbracoMapper umbracoMapper)
{
_trackedReferencesSkipTakeService = trackedReferencesSkipTakeService;
_umbracoMapper = umbracoMapper;
}
/// <summary>
/// Gets a page list of the child nodes of the current item used in any kind of relation.
/// </summary>
/// <remarks>
/// Used when deleting and unpublishing a single item to check if this item has any descending items that are in any
/// kind of relation.
/// This is basically finding the descending items which are children in relations.
/// </remarks>
[HttpGet("{id:guid}/referenced-descendants")]
[MapToApiVersion("1.0")]
[ProducesResponseType(typeof(PagedViewModel<ReferenceByIdModel>), StatusCodes.Status200OK)]
public async Task<ActionResult<PagedViewModel<ReferenceByIdModel>>> ReferencedDescendants(
CancellationToken cancellationToken,
Guid id,
int skip = 0,
int take = 20)
{
PagedModel<RelationItemModel> relationItems = await _trackedReferencesSkipTakeService.GetPagedDescendantsInReferencesAsync(id, skip, take, true);
var pagedViewModel = new PagedViewModel<ReferenceByIdModel>
{
Total = relationItems.Total,
Items = _umbracoMapper.MapEnumerable<RelationItemModel, ReferenceByIdModel>(relationItems.Items),
};
return pagedViewModel;
}
}
@@ -21,7 +21,7 @@ public class RebuildPublishedCacheController : PublishedCacheControllerBase
{
var problemDetails = new ProblemDetails
{
Title = "Database cache can not be rebuilt",
Title = "Database cache cannot be rebuilt",
Detail = $"The database cache is in the process of rebuilding.",
Status = StatusCodes.Status400BadRequest,
Type = "Error",
@@ -21,6 +21,10 @@ public abstract class SecurityControllerBase : ManagementApiControllerBase
.WithTitle("The password reset token was invalid")
.WithDetail("The specified password reset token was either used already or wrong.")
.Build()),
UserOperationStatus.CancelledByNotification => BadRequest(problemDetailsBuilder
.WithTitle("Cancelled by notification")
.WithDetail("A notification handler prevented the user operation.")
.Build()),
UserOperationStatus.UnknownFailure => BadRequest(problemDetailsBuilder
.WithTitle("Unknown failure")
.WithDetail(errorMessageResult?.Error?.ErrorMessage ?? "The error was unknown")
@@ -1,4 +1,4 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Api.Common.Builders;
using Umbraco.Cms.Api.Management.Routing;
@@ -17,6 +17,9 @@ public abstract class TemporaryFileControllerBase : ManagementApiControllerBase
.WithTitle("File extension not allowed")
.WithDetail("The file extension is not allowed.")
.Build()),
TemporaryFileOperationStatus.InvalidFileName => BadRequest(problemDetailsBuilder
.WithTitle("The provided file name is not valid")
.Build()),
TemporaryFileOperationStatus.KeyAlreadyUsed => BadRequest(problemDetailsBuilder
.WithTitle("Key already used")
.WithDetail("The specified key is already used.")
@@ -42,11 +42,21 @@ public abstract class UserStartNodeTreeControllerBase<TItem> : EntityTreeControl
protected override IEntitySlim[] GetPagedChildEntities(Guid parentKey, int skip, int take, out long totalItems)
{
IEntitySlim[] children = base.GetPagedChildEntities(parentKey, skip, take, out totalItems);
return UserHasRootAccess() || IgnoreUserStartNodes()
? children
// Keeping the correct totalItems amount from GetPagedChildEntities
: CalculateAccessMap(() => _userStartNodeEntitiesService.ChildUserAccessEntities(children, UserStartNodePaths), out _);
if (UserHasRootAccess() || IgnoreUserStartNodes())
{
return base.GetPagedChildEntities(parentKey, skip, take, out totalItems);
}
IEnumerable<UserAccessEntity> userAccessEntities = _userStartNodeEntitiesService.ChildUserAccessEntities(
ItemObjectType,
UserStartNodePaths,
parentKey,
skip,
take,
ItemOrdering,
out totalItems);
return CalculateAccessMap(() => userAccessEntities, out _);
}
protected override TItem[] MapTreeItemViewModels(Guid? parentKey, IEntitySlim[] entities)
@@ -1,15 +1,12 @@
using Asp.Versioning;
using Microsoft.AspNetCore.Authorization;
using Asp.Versioning;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Api.Management.Factories;
using Umbraco.Cms.Api.Management.ViewModels.User;
using Umbraco.Cms.Web.Common.Authorization;
namespace Umbraco.Cms.Api.Management.Controllers.User;
[ApiVersion("1.0")]
[Authorize(Policy = AuthorizationPolicies.RequireAdminAccess)]
public class ConfigurationUserController : UserControllerBase
{
private readonly IUserPresentationFactory _userPresentationFactory;
@@ -1,3 +1,4 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Api.Common.ViewModels.Pagination;
using Umbraco.Cms.Api.Management.Factories;
@@ -5,11 +6,13 @@ using Umbraco.Cms.Api.Management.Routing;
using Umbraco.Cms.Api.Management.ViewModels.Webhook.Logs;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Web.Common.Authorization;
namespace Umbraco.Cms.Api.Management.Controllers.Webhook.Logs;
[VersionedApiBackOfficeRoute($"{Constants.UdiEntityType.Webhook}")]
[ApiExplorerSettings(GroupName = "Webhook")]
[Authorize(Policy = AuthorizationPolicies.TreeAccessWebhooks)]
public class WebhookLogControllerBase : ManagementApiControllerBase
{
protected PagedViewModel<WebhookLogResponseModel> CreatePagedWebhookLogResponseModel(PagedModel<WebhookLog> logs, IWebhookPresentationFactory webhookPresentationFactory)
@@ -1,5 +1,8 @@
using Microsoft.Extensions.DependencyInjection;
using Umbraco.Cms.Api.Management.ViewModels;
using Umbraco.Cms.Api.Management.ViewModels.Document;
using Umbraco.Cms.Api.Management.ViewModels.Document.Collection;
using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Core.Mapping;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Services;
@@ -9,11 +12,23 @@ namespace Umbraco.Cms.Api.Management.Factories;
public class DocumentCollectionPresentationFactory : ContentCollectionPresentationFactory<IContent, DocumentCollectionResponseModel, DocumentValueResponseModel, DocumentVariantResponseModel>, IDocumentCollectionPresentationFactory
{
private readonly IPublicAccessService _publicAccessService;
private readonly IEntityService _entityService;
[Obsolete("Please use the non-obsolete constructor. Scheduled for removal in V17.")]
public DocumentCollectionPresentationFactory(IUmbracoMapper mapper, IPublicAccessService publicAccessService)
: base(mapper)
: this(
mapper,
publicAccessService,
StaticServiceProvider.Instance.GetRequiredService<IEntityService>())
{
}
[ActivatorUtilitiesConstructor]
public DocumentCollectionPresentationFactory(IUmbracoMapper mapper, IPublicAccessService publicAccessService, IEntityService entityService)
: base(mapper)
{
_publicAccessService = publicAccessService;
_entityService = entityService;
}
protected override Task SetUnmappedProperties(ListViewPagedModel<IContent> contentCollection, List<DocumentCollectionResponseModel> collectionResponseModels)
@@ -27,6 +42,8 @@ public class DocumentCollectionPresentationFactory : ContentCollectionPresentati
}
item.IsProtected = _publicAccessService.IsProtected(matchingContentItem).Success;
item.Ancestors = _entityService.GetPathKeys(matchingContentItem, omitSelf: true)
.Select(x => new ReferenceByIdModel(x));
}
return Task.CompletedTask;
@@ -1,4 +1,4 @@
using Umbraco.Cms.Api.Management.ViewModels;
using Umbraco.Cms.Api.Management.ViewModels;
using Umbraco.Cms.Api.Management.ViewModels.RedirectUrlManagement;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Routing;
@@ -22,6 +22,12 @@ public class RedirectUrlPresentationFactory : IRedirectUrlPresentationFactory
var originalUrl = _publishedUrlProvider.GetUrlFromRoute(source.ContentId, source.Url, source.Culture);
// Even if the URL could not be extracted from the route, if we have a path as a the route for the original URL, we should display it.
if (originalUrl == "#" && source.Url.StartsWith('/'))
{
originalUrl = source.Url;
}
return new RedirectUrlResponseModel
{
OriginalUrl = originalUrl,
@@ -1,7 +1,12 @@
using Umbraco.Cms.Api.Management.ViewModels.TrackedReferences;
using Microsoft.Extensions.DependencyInjection;
using Umbraco.Cms.Api.Management.ViewModels.TrackedReferences;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Core.Mapping;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.Entities;
using Umbraco.Cms.Core.Persistence.Repositories;
using Umbraco.Cms.Infrastructure.Scoping;
using Umbraco.Extensions;
namespace Umbraco.Cms.Api.Management.Factories;
@@ -9,21 +14,73 @@ namespace Umbraco.Cms.Api.Management.Factories;
public class RelationTypePresentationFactory : IRelationTypePresentationFactory
{
private readonly IUmbracoMapper _umbracoMapper;
private readonly IEntityRepository _entityRepository;
private readonly IDocumentPresentationFactory _documentPresentationFactory;
private readonly IScopeProvider _scopeProvider;
[Obsolete("Please use the non obsoleted constructor. Scheduled for removal in v17")]
public RelationTypePresentationFactory(IUmbracoMapper umbracoMapper)
: this(
umbracoMapper,
StaticServiceProvider.Instance.GetRequiredService<IEntityRepository>(),
StaticServiceProvider.Instance.GetRequiredService<IDocumentPresentationFactory>(),
StaticServiceProvider.Instance.GetRequiredService<IScopeProvider>())
{
_umbracoMapper = umbracoMapper;
}
public async Task<IEnumerable<IReferenceResponseModel>> CreateReferenceResponseModelsAsync(IEnumerable<RelationItemModel> relationItemModels)
public RelationTypePresentationFactory(
IUmbracoMapper umbracoMapper,
IEntityRepository entityRepository,
IDocumentPresentationFactory documentPresentationFactory,
IScopeProvider scopeProvider)
{
IReferenceResponseModel[] result = relationItemModels.Select(relationItemModel => relationItemModel.NodeType switch
{
Constants.UdiEntityType.Document => _umbracoMapper.Map<DocumentReferenceResponseModel>(relationItemModel),
Constants.UdiEntityType.Media => _umbracoMapper.Map<MediaReferenceResponseModel>(relationItemModel),
_ => _umbracoMapper.Map<DefaultReferenceResponseModel>(relationItemModel) as IReferenceResponseModel,
}).WhereNotNull().ToArray();
_umbracoMapper = umbracoMapper;
_entityRepository = entityRepository;
_documentPresentationFactory = documentPresentationFactory;
_scopeProvider = scopeProvider;
}
public async Task<IEnumerable<IReferenceResponseModel>> CreateReferenceResponseModelsAsync(
IEnumerable<RelationItemModel> relationItemModels)
{
IReadOnlyCollection<RelationItemModel> relationItemModelsCollection = relationItemModels.ToArray();
Guid[] documentKeys = relationItemModelsCollection
.Where(item => item.NodeType is Constants.UdiEntityType.Document)
.Select(item => item.NodeKey).Distinct().ToArray();
using IScope scope = _scopeProvider.CreateScope(autoComplete: true);
var slimEntities = _entityRepository.GetAll(Constants.ObjectTypes.Document, documentKeys).ToList();
IReferenceResponseModel[] result = relationItemModelsCollection.Select(relationItemModel =>
relationItemModel.NodeType switch
{
Constants.ReferenceType.Document => MapDocumentReference(relationItemModel, slimEntities),
Constants.ReferenceType.Media => _umbracoMapper.Map<MediaReferenceResponseModel>(relationItemModel),
Constants.ReferenceType.Member => _umbracoMapper.Map<MemberReferenceResponseModel>(relationItemModel),
Constants.ReferenceType.DocumentTypePropertyType => _umbracoMapper.Map<DocumentTypePropertyTypeReferenceResponseModel>(relationItemModel),
Constants.ReferenceType.MediaTypePropertyType => _umbracoMapper.Map<MediaTypePropertyTypeReferenceResponseModel>(relationItemModel),
Constants.ReferenceType.MemberTypePropertyType => _umbracoMapper.Map<MemberTypePropertyTypeReferenceResponseModel>(relationItemModel),
_ => _umbracoMapper.Map<DefaultReferenceResponseModel>(relationItemModel),
}).WhereNotNull().ToArray();
return await Task.FromResult(result);
}
private IReferenceResponseModel? MapDocumentReference(RelationItemModel relationItemModel,
List<IEntitySlim> slimEntities)
{
DocumentReferenceResponseModel? documentReferenceResponseModel =
_umbracoMapper.Map<DocumentReferenceResponseModel>(relationItemModel);
if (documentReferenceResponseModel is not null
&& slimEntities.FirstOrDefault(e => e.Key == relationItemModel.NodeKey) is DocumentEntitySlim
matchingSlimDocument)
{
documentReferenceResponseModel.Variants =
_documentPresentationFactory.CreateVariantsItemResponseModels(matchingSlimDocument);
}
return documentReferenceResponseModel;
}
}
@@ -1,13 +1,17 @@
using Umbraco.Cms.Api.Management.Routing;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using Umbraco.Cms.Api.Management.Routing;
using Umbraco.Cms.Api.Management.Security;
using Umbraco.Cms.Api.Management.ViewModels;
using Umbraco.Cms.Api.Management.ViewModels.User;
using Umbraco.Cms.Api.Management.ViewModels.User.Current;
using Umbraco.Cms.Core;
using Umbraco.Cms.Api.Management.ViewModels.User.Item;
using Umbraco.Cms.Api.Management.ViewModels.UserGroup;
using Umbraco.Cms.Api.Management.ViewModels.UserGroup.Permissions;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Cache;
using Umbraco.Cms.Core.Configuration.Models;
using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Core.IO;
using Umbraco.Cms.Core.Mail;
using Umbraco.Cms.Core.Media;
@@ -20,7 +24,6 @@ namespace Umbraco.Cms.Api.Management.Factories;
public class UserPresentationFactory : IUserPresentationFactory
{
private readonly IEntityService _entityService;
private readonly AppCaches _appCaches;
private readonly MediaFileManager _mediaFileManager;
@@ -31,7 +34,10 @@ public class UserPresentationFactory : IUserPresentationFactory
private readonly IPasswordConfigurationPresentationFactory _passwordConfigurationPresentationFactory;
private readonly IBackOfficeExternalLoginProviders _externalLoginProviders;
private readonly SecuritySettings _securitySettings;
private readonly IUserService _userService;
private readonly IContentService _contentService;
[Obsolete("Please use the constructor taking all parameters. Scheduled for removal in Umbraco 17.")]
public UserPresentationFactory(
IEntityService entityService,
AppCaches appCaches,
@@ -43,6 +49,35 @@ public class UserPresentationFactory : IUserPresentationFactory
IPasswordConfigurationPresentationFactory passwordConfigurationPresentationFactory,
IOptionsSnapshot<SecuritySettings> securitySettings,
IBackOfficeExternalLoginProviders externalLoginProviders)
: this(
entityService,
appCaches,
mediaFileManager,
imageUrlGenerator,
userGroupPresentationFactory,
absoluteUrlBuilder,
emailSender,
passwordConfigurationPresentationFactory,
securitySettings,
externalLoginProviders,
StaticServiceProvider.Instance.GetRequiredService<IUserService>(),
StaticServiceProvider.Instance.GetRequiredService<IContentService>())
{
}
public UserPresentationFactory(
IEntityService entityService,
AppCaches appCaches,
MediaFileManager mediaFileManager,
IImageUrlGenerator imageUrlGenerator,
IUserGroupPresentationFactory userGroupPresentationFactory,
IAbsoluteUrlBuilder absoluteUrlBuilder,
IEmailSender emailSender,
IPasswordConfigurationPresentationFactory passwordConfigurationPresentationFactory,
IOptionsSnapshot<SecuritySettings> securitySettings,
IBackOfficeExternalLoginProviders externalLoginProviders,
IUserService userService,
IContentService contentService)
{
_entityService = entityService;
_appCaches = appCaches;
@@ -54,6 +89,8 @@ public class UserPresentationFactory : IUserPresentationFactory
_externalLoginProviders = externalLoginProviders;
_securitySettings = securitySettings.Value;
_absoluteUrlBuilder = absoluteUrlBuilder;
_userService = userService;
_contentService = contentService;
}
public UserResponseModel CreateResponseModel(IUser user)
@@ -195,7 +232,7 @@ public class UserPresentationFactory : IUserPresentationFactory
var contentStartNodeIds = user.CalculateContentStartNodeIds(_entityService, _appCaches);
var documentStartNodeKeys = GetKeysFromIds(contentStartNodeIds, UmbracoObjectTypes.Document);
var permissions = presentationGroups.SelectMany(x => x.Permissions).ToHashSet();
var permissions = GetAggregatedGranularPermissions(user, presentationGroups);
var fallbackPermissions = presentationGroups.SelectMany(x => x.FallbackPermissions).ToHashSet();
var hasAccessToAllLanguages = presentationGroups.Any(x => x.HasAccessToAllLanguages);
@@ -225,6 +262,42 @@ public class UserPresentationFactory : IUserPresentationFactory
});
}
private HashSet<IPermissionPresentationModel> GetAggregatedGranularPermissions(IUser user, IEnumerable<UserGroupResponseModel> presentationGroups)
{
var permissions = presentationGroups.SelectMany(x => x.Permissions).ToHashSet();
// The raw permission data consists of several permissions for each document. We want to aggregate this server-side so
// we return one set of aggregate permissions per document that the client will use.
// Get the unique document keys that have granular permissions.
IEnumerable<Guid> documentKeysWithGranularPermissions = permissions
.Where(x => x is DocumentPermissionPresentationModel)
.Cast<DocumentPermissionPresentationModel>()
.Select(x => x.Document.Id)
.Distinct();
var aggregatedPermissions = new HashSet<IPermissionPresentationModel>();
foreach (Guid documentKey in documentKeysWithGranularPermissions)
{
// Retrieve the path of the document.
var path = _contentService.GetById(documentKey)?.Path;
if (string.IsNullOrEmpty(path))
{
continue;
}
// With the path we can call the same logic as used server-side for authorizing access to resources.
EntityPermissionSet permissionsForPath = _userService.GetPermissionsForPath(user, path);
aggregatedPermissions.Add(new DocumentPermissionPresentationModel
{
Document = new ReferenceByIdModel(documentKey),
Verbs = permissionsForPath.GetAllPermissions()
});
}
return aggregatedPermissions;
}
public async Task<CalculatedUserStartNodesResponseModel> CreateCalculatedUserStartNodesResponseModelAsync(IUser user)
{
var mediaStartNodeIds = user.CalculateMediaStartNodeIds(_entityService, _appCaches);
@@ -67,7 +67,7 @@ public class DocumentMapDefinition : ContentMapDefinition<IContent, DocumentValu
target.IsTrashed = source.Trashed;
}
// Umbraco.Code.MapAll -IsProtected
// Umbraco.Code.MapAll -IsProtected -Ancestors
private void Map(IContent source, DocumentCollectionResponseModel target, MapperContext context)
{
target.Id = source.Key;
@@ -1,4 +1,4 @@
using Umbraco.Cms.Api.Management.ViewModels;
using Umbraco.Cms.Api.Management.ViewModels;
using Umbraco.Cms.Core.Mapping;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Api.Management.ViewModels.TrackedReferences;
@@ -11,12 +11,16 @@ public class TrackedReferenceViewModelsMapDefinition : IMapDefinition
{
mapper.Define<RelationItemModel, DocumentReferenceResponseModel>((source, context) => new DocumentReferenceResponseModel(), Map);
mapper.Define<RelationItemModel, MediaReferenceResponseModel>((source, context) => new MediaReferenceResponseModel(), Map);
mapper.Define<RelationItemModel, MemberReferenceResponseModel>((source, context) => new MemberReferenceResponseModel(), Map);
mapper.Define<RelationItemModel, DocumentTypePropertyTypeReferenceResponseModel>((source, context) => new DocumentTypePropertyTypeReferenceResponseModel(), Map);
mapper.Define<RelationItemModel, MediaTypePropertyTypeReferenceResponseModel>((source, context) => new MediaTypePropertyTypeReferenceResponseModel(), Map);
mapper.Define<RelationItemModel, MemberTypePropertyTypeReferenceResponseModel>((source, context) => new MemberTypePropertyTypeReferenceResponseModel(), Map);
mapper.Define<RelationItemModel, DefaultReferenceResponseModel>((source, context) => new DefaultReferenceResponseModel(), Map);
mapper.Define<RelationItemModel, ReferenceByIdModel>((source, context) => new ReferenceByIdModel(), Map);
mapper.Define<Guid, ReferenceByIdModel>((source, context) => new ReferenceByIdModel(), Map);
}
// Umbraco.Code.MapAll
// Umbraco.Code.MapAll -Variants
private void Map(RelationItemModel source, DocumentReferenceResponseModel target, MapperContext context)
{
target.Id = source.NodeKey;
@@ -24,6 +28,7 @@ public class TrackedReferenceViewModelsMapDefinition : IMapDefinition
target.Published = source.NodePublished;
target.DocumentType = new TrackedReferenceDocumentType
{
Id = source.ContentTypeKey,
Alias = source.ContentTypeAlias,
Icon = source.ContentTypeIcon,
Name = source.ContentTypeName,
@@ -37,6 +42,66 @@ public class TrackedReferenceViewModelsMapDefinition : IMapDefinition
target.Name = source.NodeName;
target.MediaType = new TrackedReferenceMediaType
{
Id = source.ContentTypeKey,
Alias = source.ContentTypeAlias,
Icon = source.ContentTypeIcon,
Name = source.ContentTypeName,
};
}
// Umbraco.Code.MapAll
private void Map(RelationItemModel source, MemberReferenceResponseModel target, MapperContext context)
{
target.Id = source.NodeKey;
target.Name = source.NodeName;
target.MemberType = new TrackedReferenceMemberType
{
Id = source.ContentTypeKey,
Alias = source.ContentTypeAlias,
Icon = source.ContentTypeIcon,
Name = source.ContentTypeName,
};
}
// Umbraco.Code.MapAll
private void Map(RelationItemModel source, DocumentTypePropertyTypeReferenceResponseModel target, MapperContext context)
{
target.Id = source.NodeKey;
target.Name = source.NodeName;
target.Alias = source.NodeAlias;
target.DocumentType = new TrackedReferenceDocumentType
{
Id = source.ContentTypeKey,
Alias = source.ContentTypeAlias,
Icon = source.ContentTypeIcon,
Name = source.ContentTypeName,
};
}
// Umbraco.Code.MapAll
private void Map(RelationItemModel source, MediaTypePropertyTypeReferenceResponseModel target, MapperContext context)
{
target.Id = source.NodeKey;
target.Name = source.NodeName;
target.Alias = source.NodeAlias;
target.MediaType = new TrackedReferenceMediaType
{
Id = source.ContentTypeKey,
Alias = source.ContentTypeAlias,
Icon = source.ContentTypeIcon,
Name = source.ContentTypeName,
};
}
// Umbraco.Code.MapAll
private void Map(RelationItemModel source, MemberTypePropertyTypeReferenceResponseModel target, MapperContext context)
{
target.Id = source.NodeKey;
target.Name = source.NodeName;
target.Alias = source.NodeAlias;
target.MemberType = new TrackedReferenceMemberType
{
Id = source.ContentTypeKey,
Alias = source.ContentTypeAlias,
Icon = source.ContentTypeIcon,
Name = source.ContentTypeName,
+799 -2
View File
@@ -816,6 +816,70 @@
]
}
},
"/umbraco/management/api/v1/data-type/{id}/referenced-by": {
"get": {
"tags": [
"Data Type"
],
"operationId": "GetDataTypeByIdReferencedBy",
"parameters": [
{
"name": "id",
"in": "path",
"required": true,
"schema": {
"type": "string",
"format": "uuid"
}
},
{
"name": "skip",
"in": "query",
"schema": {
"type": "integer",
"format": "int32",
"default": 0
}
},
{
"name": "take",
"in": "query",
"schema": {
"type": "integer",
"format": "int32",
"default": 20
}
}
],
"responses": {
"200": {
"description": "OK",
"content": {
"application/json": {
"schema": {
"oneOf": [
{
"$ref": "#/components/schemas/PagedIReferenceResponseModel"
}
]
}
}
}
},
"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/data-type/{id}/references": {
"get": {
"tags": [
@@ -872,6 +936,7 @@
"description": "The authenticated user does not have access to this resource"
}
},
"deprecated": true,
"security": [
{
"Backoffice User": [ ]
@@ -3420,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": [
@@ -6245,6 +6370,13 @@
"type": "string"
}
},
{
"name": "isElement",
"in": "query",
"schema": {
"type": "boolean"
}
},
{
"name": "skip",
"in": "query",
@@ -8895,6 +9027,17 @@
"nullable": true
}
}
},
"content": {
"application/json": {
"schema": {
"oneOf": [
{
"$ref": "#/components/schemas/PublishWithDescendantsResultModel"
}
]
}
}
}
},
"400": {
@@ -8975,6 +9118,89 @@
]
}
},
"/umbraco/management/api/v1/document/{id}/publish-with-descendants/result/{taskId}": {
"get": {
"tags": [
"Document"
],
"operationId": "GetDocumentByIdPublishWithDescendantsResultByTaskId",
"parameters": [
{
"name": "id",
"in": "path",
"required": true,
"schema": {
"type": "string",
"format": "uuid"
}
},
{
"name": "taskId",
"in": "path",
"required": true,
"schema": {
"type": "string",
"format": "uuid"
}
}
],
"responses": {
"200": {
"description": "OK",
"content": {
"application/json": {
"schema": {
"oneOf": [
{
"$ref": "#/components/schemas/PublishWithDescendantsResultModel"
}
]
}
}
}
},
"400": {
"description": "Bad Request",
"content": {
"application/json": {
"schema": {
"oneOf": [
{
"$ref": "#/components/schemas/ProblemDetails"
}
]
}
}
}
},
"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/{id}/published": {
"get": {
"tags": [
@@ -10069,6 +10295,13 @@
"type": "string"
}
},
{
"name": "trashed",
"in": "query",
"schema": {
"type": "boolean"
}
},
{
"name": "skip",
"in": "query",
@@ -10597,6 +10830,61 @@
]
}
},
"/umbraco/management/api/v1/recycle-bin/document/referenced-by": {
"get": {
"tags": [
"Document"
],
"operationId": "GetRecycleBinDocumentReferencedBy",
"parameters": [
{
"name": "skip",
"in": "query",
"schema": {
"type": "integer",
"format": "int32",
"default": 0
}
},
{
"name": "take",
"in": "query",
"schema": {
"type": "integer",
"format": "int32",
"default": 20
}
}
],
"responses": {
"200": {
"description": "OK",
"content": {
"application/json": {
"schema": {
"oneOf": [
{
"$ref": "#/components/schemas/PagedIReferenceResponseModel"
}
]
}
}
}
},
"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/recycle-bin/document/root": {
"get": {
"tags": [
@@ -15766,6 +16054,13 @@
"type": "string"
}
},
{
"name": "trashed",
"in": "query",
"schema": {
"type": "boolean"
}
},
{
"name": "skip",
"in": "query",
@@ -17741,6 +18036,61 @@
]
}
},
"/umbraco/management/api/v1/recycle-bin/media/referenced-by": {
"get": {
"tags": [
"Media"
],
"operationId": "GetRecycleBinMediaReferencedBy",
"parameters": [
{
"name": "skip",
"in": "query",
"schema": {
"type": "integer",
"format": "int32",
"default": 0
}
},
{
"name": "take",
"in": "query",
"schema": {
"type": "integer",
"format": "int32",
"default": 20
}
}
],
"responses": {
"200": {
"description": "OK",
"content": {
"application/json": {
"schema": {
"oneOf": [
{
"$ref": "#/components/schemas/PagedIReferenceResponseModel"
}
]
}
}
}
},
"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/recycle-bin/media/root": {
"get": {
"tags": [
@@ -20170,6 +20520,134 @@
]
}
},
"/umbraco/management/api/v1/member/{id}/referenced-by": {
"get": {
"tags": [
"Member"
],
"operationId": "GetMemberByIdReferencedBy",
"parameters": [
{
"name": "id",
"in": "path",
"required": true,
"schema": {
"type": "string",
"format": "uuid"
}
},
{
"name": "skip",
"in": "query",
"schema": {
"type": "integer",
"format": "int32",
"default": 0
}
},
{
"name": "take",
"in": "query",
"schema": {
"type": "integer",
"format": "int32",
"default": 20
}
}
],
"responses": {
"200": {
"description": "OK",
"content": {
"application/json": {
"schema": {
"oneOf": [
{
"$ref": "#/components/schemas/PagedIReferenceResponseModel"
}
]
}
}
}
},
"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/member/{id}/referenced-descendants": {
"get": {
"tags": [
"Member"
],
"operationId": "GetMemberByIdReferencedDescendants",
"parameters": [
{
"name": "id",
"in": "path",
"required": true,
"schema": {
"type": "string",
"format": "uuid"
}
},
{
"name": "skip",
"in": "query",
"schema": {
"type": "integer",
"format": "int32",
"default": 0
}
},
{
"name": "take",
"in": "query",
"schema": {
"type": "integer",
"format": "int32",
"default": 20
}
}
],
"responses": {
"200": {
"description": "OK",
"content": {
"application/json": {
"schema": {
"oneOf": [
{
"$ref": "#/components/schemas/PagedReferenceByIdModel"
}
]
}
}
}
},
"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/member/{id}/validate": {
"put": {
"tags": [
@@ -20312,6 +20790,73 @@
]
}
},
"/umbraco/management/api/v1/member/are-referenced": {
"get": {
"tags": [
"Member"
],
"operationId": "GetMemberAreReferenced",
"parameters": [
{
"name": "id",
"in": "query",
"schema": {
"uniqueItems": true,
"type": "array",
"items": {
"type": "string",
"format": "uuid"
}
}
},
{
"name": "skip",
"in": "query",
"schema": {
"type": "integer",
"format": "int32",
"default": 0
}
},
{
"name": "take",
"in": "query",
"schema": {
"type": "integer",
"format": "int32",
"default": 20
}
}
],
"responses": {
"200": {
"description": "OK",
"content": {
"application/json": {
"schema": {
"oneOf": [
{
"$ref": "#/components/schemas/PagedReferenceByIdModel"
}
]
}
}
}
},
"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/member/configuration": {
"get": {
"tags": [
@@ -36696,7 +37241,6 @@
"type": "string"
},
"translation": {
"minLength": 1,
"type": "string"
}
},
@@ -36872,6 +37416,7 @@
},
"DocumentCollectionResponseModel": {
"required": [
"ancestors",
"documentType",
"id",
"isProtected",
@@ -36927,6 +37472,16 @@
"isProtected": {
"type": "boolean"
},
"ancestors": {
"type": "array",
"items": {
"oneOf": [
{
"$ref": "#/components/schemas/ReferenceByIdModel"
}
]
}
},
"updater": {
"type": "string",
"nullable": true
@@ -37126,7 +37681,8 @@
"required": [
"$type",
"documentType",
"id"
"id",
"variants"
],
"type": "object",
"properties": {
@@ -37151,6 +37707,16 @@
"$ref": "#/components/schemas/TrackedReferenceDocumentTypeModel"
}
]
},
"variants": {
"type": "array",
"items": {
"oneOf": [
{
"$ref": "#/components/schemas/DocumentVariantItemResponseModel"
}
]
}
}
},
"additionalProperties": false,
@@ -37229,6 +37795,7 @@
},
"DocumentTreeItemResponseModel": {
"required": [
"ancestors",
"createDate",
"documentType",
"hasChildren",
@@ -37268,6 +37835,16 @@
"isProtected": {
"type": "boolean"
},
"ancestors": {
"type": "array",
"items": {
"oneOf": [
{
"$ref": "#/components/schemas/ReferenceByIdModel"
}
]
}
},
"documentType": {
"oneOf": [
{
@@ -37513,6 +38090,45 @@
},
"additionalProperties": false
},
"DocumentTypePropertyTypeReferenceResponseModel": {
"required": [
"$type",
"documentType",
"id"
],
"type": "object",
"properties": {
"$type": {
"type": "string"
},
"id": {
"type": "string",
"format": "uuid"
},
"name": {
"type": "string",
"nullable": true
},
"alias": {
"type": "string",
"nullable": true
},
"documentType": {
"oneOf": [
{
"$ref": "#/components/schemas/TrackedReferenceDocumentTypeModel"
}
]
}
},
"additionalProperties": false,
"discriminator": {
"propertyName": "$type",
"mapping": {
"DocumentTypePropertyTypeReferenceResponseModel": "#/components/schemas/DocumentTypePropertyTypeReferenceResponseModel"
}
}
},
"DocumentTypePropertyTypeResponseModel": {
"required": [
"alias",
@@ -39577,6 +40193,45 @@
},
"additionalProperties": false
},
"MediaTypePropertyTypeReferenceResponseModel": {
"required": [
"$type",
"id",
"mediaType"
],
"type": "object",
"properties": {
"$type": {
"type": "string"
},
"id": {
"type": "string",
"format": "uuid"
},
"name": {
"type": "string",
"nullable": true
},
"alias": {
"type": "string",
"nullable": true
},
"mediaType": {
"oneOf": [
{
"$ref": "#/components/schemas/TrackedReferenceMediaTypeModel"
}
]
}
},
"additionalProperties": false,
"discriminator": {
"propertyName": "$type",
"mapping": {
"MediaTypePropertyTypeReferenceResponseModel": "#/components/schemas/MediaTypePropertyTypeReferenceResponseModel"
}
}
},
"MediaTypePropertyTypeResponseModel": {
"required": [
"alias",
@@ -40084,6 +40739,41 @@
],
"type": "string"
},
"MemberReferenceResponseModel": {
"required": [
"$type",
"id",
"memberType"
],
"type": "object",
"properties": {
"$type": {
"type": "string"
},
"id": {
"type": "string",
"format": "uuid"
},
"name": {
"type": "string",
"nullable": true
},
"memberType": {
"oneOf": [
{
"$ref": "#/components/schemas/TrackedReferenceMemberTypeModel"
}
]
}
},
"additionalProperties": false,
"discriminator": {
"propertyName": "$type",
"mapping": {
"MemberReferenceResponseModel": "#/components/schemas/MemberReferenceResponseModel"
}
}
},
"MemberResponseModel": {
"required": [
"email",
@@ -40320,6 +41010,45 @@
},
"additionalProperties": false
},
"MemberTypePropertyTypeReferenceResponseModel": {
"required": [
"$type",
"id",
"memberType"
],
"type": "object",
"properties": {
"$type": {
"type": "string"
},
"id": {
"type": "string",
"format": "uuid"
},
"name": {
"type": "string",
"nullable": true
},
"alias": {
"type": "string",
"nullable": true
},
"memberType": {
"oneOf": [
{
"$ref": "#/components/schemas/TrackedReferenceMemberTypeModel"
}
]
}
},
"additionalProperties": false,
"discriminator": {
"propertyName": "$type",
"mapping": {
"MemberTypePropertyTypeReferenceResponseModel": "#/components/schemas/MemberTypePropertyTypeReferenceResponseModel"
}
}
},
"MemberTypePropertyTypeResponseModel": {
"required": [
"alias",
@@ -41492,8 +42221,20 @@
{
"$ref": "#/components/schemas/DocumentReferenceResponseModel"
},
{
"$ref": "#/components/schemas/DocumentTypePropertyTypeReferenceResponseModel"
},
{
"$ref": "#/components/schemas/MediaReferenceResponseModel"
},
{
"$ref": "#/components/schemas/MediaTypePropertyTypeReferenceResponseModel"
},
{
"$ref": "#/components/schemas/MemberReferenceResponseModel"
},
{
"$ref": "#/components/schemas/MemberTypePropertyTypeReferenceResponseModel"
}
]
}
@@ -42867,6 +43608,23 @@
},
"additionalProperties": false
},
"PublishWithDescendantsResultModel": {
"required": [
"isComplete",
"taskId"
],
"type": "object",
"properties": {
"taskId": {
"type": "string",
"format": "uuid"
},
"isComplete": {
"type": "boolean"
}
},
"additionalProperties": false
},
"PublishedDocumentResponseModel": {
"required": [
"documentType",
@@ -44131,8 +44889,15 @@
"additionalProperties": false
},
"TrackedReferenceDocumentTypeModel": {
"required": [
"id"
],
"type": "object",
"properties": {
"id": {
"type": "string",
"format": "uuid"
},
"icon": {
"type": "string",
"nullable": true
@@ -44149,8 +44914,40 @@
"additionalProperties": false
},
"TrackedReferenceMediaTypeModel": {
"required": [
"id"
],
"type": "object",
"properties": {
"id": {
"type": "string",
"format": "uuid"
},
"icon": {
"type": "string",
"nullable": true
},
"alias": {
"type": "string",
"nullable": true
},
"name": {
"type": "string",
"nullable": true
}
},
"additionalProperties": false
},
"TrackedReferenceMemberTypeModel": {
"required": [
"id"
],
"type": "object",
"properties": {
"id": {
"type": "string",
"format": "uuid"
},
"icon": {
"type": "string",
"nullable": true
@@ -38,7 +38,7 @@ public abstract class BackOfficeSecurityRequirementsOperationFilterBase : IOpera
Type = ReferenceType.SecurityScheme,
Id = ManagementApiConfiguration.ApiSecurityName
}
}, new string[] { }
}, []
}
}
};
@@ -16,9 +16,41 @@ public interface IUserStartNodeEntitiesService
/// <remarks>
/// The returned entities may include entities that outside of the user start node scope, but are needed to
/// for browsing to the actual user start nodes. These entities will be marked as "no access" entities.
///
/// This method does not support pagination, because it must load all entities explicitly in order to calculate
/// the correct result, given that user start nodes can be descendants of root nodes. Consumers need to apply
/// pagination to the result if applicable.
/// </remarks>
IEnumerable<UserAccessEntity> RootUserAccessEntities(UmbracoObjectTypes umbracoObjectType, int[] userStartNodeIds);
/// <summary>
/// Calculates the applicable child entities for a given object type for users without root access.
/// </summary>
/// <param name="umbracoObjectType">The object type.</param>
/// <param name="userStartNodePaths">The calculated start node paths for the user.</param>
/// <param name="parentKey">The key of the parent.</param>
/// <param name="skip">The number of applicable children to skip.</param>
/// <param name="take">The number of applicable children to take.</param>
/// <param name="ordering">The ordering to apply when fetching and paginating the children.</param>
/// <param name="totalItems">The total number of applicable children available.</param>
/// <returns>A list of child entities applicable for the user.</returns>
/// <remarks>
/// The returned entities may include entities that outside of the user start node scope, but are needed to
/// for browsing to the actual user start nodes. These entities will be marked as "no access" entities.
/// </remarks>
IEnumerable<UserAccessEntity> ChildUserAccessEntities(
UmbracoObjectTypes umbracoObjectType,
string[] userStartNodePaths,
Guid parentKey,
int skip,
int take,
Ordering ordering,
out long totalItems)
{
totalItems = 0;
return [];
}
/// <summary>
/// Calculates the applicable child entities from a list of candidate child entities for users without root access.
/// </summary>
@@ -1,8 +1,12 @@
using Umbraco.Cms.Core;
using Microsoft.Extensions.DependencyInjection;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.Entities;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Api.Management.Models.Entities;
using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Core.Persistence.Querying;
using Umbraco.Cms.Core.Scoping;
using Umbraco.Extensions;
namespace Umbraco.Cms.Api.Management.Services.Entities;
@@ -10,8 +14,24 @@ namespace Umbraco.Cms.Api.Management.Services.Entities;
public class UserStartNodeEntitiesService : IUserStartNodeEntitiesService
{
private readonly IEntityService _entityService;
private readonly ICoreScopeProvider _scopeProvider;
private readonly IIdKeyMap _idKeyMap;
public UserStartNodeEntitiesService(IEntityService entityService) => _entityService = entityService;
[Obsolete("Please use the non-obsolete constructor. Scheduled for removal in V17.")]
public UserStartNodeEntitiesService(IEntityService entityService)
: this(
entityService,
StaticServiceProvider.Instance.GetRequiredService<ICoreScopeProvider>(),
StaticServiceProvider.Instance.GetRequiredService<IIdKeyMap>())
{
}
public UserStartNodeEntitiesService(IEntityService entityService, ICoreScopeProvider scopeProvider, IIdKeyMap idKeyMap)
{
_entityService = entityService;
_scopeProvider = scopeProvider;
_idKeyMap = idKeyMap;
}
/// <inheritdoc />
public IEnumerable<UserAccessEntity> RootUserAccessEntities(UmbracoObjectTypes umbracoObjectType, int[] userStartNodeIds)
@@ -43,6 +63,54 @@ public class UserStartNodeEntitiesService : IUserStartNodeEntitiesService
.ToArray();
}
public IEnumerable<UserAccessEntity> ChildUserAccessEntities(UmbracoObjectTypes umbracoObjectType, string[] userStartNodePaths, Guid parentKey, int skip, int take, Ordering ordering, out long totalItems)
{
Attempt<int> parentIdAttempt = _idKeyMap.GetIdForKey(parentKey, umbracoObjectType);
if (parentIdAttempt.Success is false)
{
totalItems = 0;
return [];
}
var parentId = parentIdAttempt.Result;
IEntitySlim? parent = _entityService.Get(parentId);
if (parent is null)
{
totalItems = 0;
return [];
}
IEntitySlim[] children;
if (userStartNodePaths.Any(path => $"{parent.Path},".StartsWith($"{path},")))
{
// the requested parent is one of the user start nodes (or a descendant of one), all children are by definition allowed
children = _entityService.GetPagedChildren(parentKey, umbracoObjectType, skip, take, out totalItems, ordering: ordering).ToArray();
return ChildUserAccessEntities(children, userStartNodePaths);
}
// if one or more of the user start nodes are descendants of the requested parent, find the "next child IDs" in those user start node paths
// - e.g. given the user start node path "-1,2,3,4,5", if the requested parent ID is 3, the "next child ID" is 4.
var userStartNodePathIds = userStartNodePaths.Select(path => path.Split(Constants.CharArrays.Comma).Select(int.Parse).ToArray()).ToArray();
var allowedChildIds = userStartNodePathIds
.Where(ids => ids.Contains(parentId))
// given the previous checks, the parent ID can never be the last in the user start node path, so this is safe
.Select(ids => ids[ids.IndexOf(parentId) + 1])
.Distinct()
.ToArray();
totalItems = allowedChildIds.Length;
if (allowedChildIds.Length == 0)
{
// the requested parent is outside the scope of any user start nodes
return [];
}
// even though we know the IDs of the allowed child entities to fetch, we still use a Query to yield correctly sorted children
IQuery<IUmbracoEntity> query = _scopeProvider.CreateQuery<IUmbracoEntity>().Where(x => allowedChildIds.Contains(x.Id));
children = _entityService.GetPagedChildren(parentKey, umbracoObjectType, skip, take, out totalItems, query, ordering).ToArray();
return ChildUserAccessEntities(children, userStartNodePaths);
}
/// <inheritdoc />
public IEnumerable<UserAccessEntity> ChildUserAccessEntities(IEnumerable<IEntitySlim> candidateChildren, string[] userStartNodePaths)
// child entities for users without root access should include:
@@ -1,4 +1,4 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations;
namespace Umbraco.Cms.Api.Management.ViewModels.Dictionary;
@@ -7,6 +7,5 @@ public class DictionaryItemTranslationModel
[Required]
public string IsoCode { get; set; } = string.Empty;
[Required]
public string Translation { get; set; } = string.Empty;
}
@@ -11,5 +11,7 @@ public class DocumentCollectionResponseModel : ContentCollectionResponseModelBas
public bool IsProtected { get; set; }
public IEnumerable<ReferenceByIdModel> Ancestors { get; set; } = [];
public string? Updater { get; set; }
}
@@ -0,0 +1,8 @@
namespace Umbraco.Cms.Api.Management.ViewModels.Document;
public class PublishWithDescendantsResultModel
{
public Guid TaskId { get; set; }
public bool IsComplete { get; set; }
}
@@ -0,0 +1,6 @@
namespace Umbraco.Cms.Api.Management.ViewModels.TrackedReferences;
public abstract class ContentTypePropertyTypeReferenceResponseModel : ReferenceResponseModel
{
public string? Alias { get; set; }
}
@@ -1,11 +1,7 @@
namespace Umbraco.Cms.Api.Management.ViewModels.TrackedReferences;
namespace Umbraco.Cms.Api.Management.ViewModels.TrackedReferences;
public class DefaultReferenceResponseModel : IReferenceResponseModel
public class DefaultReferenceResponseModel : ReferenceResponseModel
{
public Guid Id { get; set; }
public string? Name { get; set; }
public string? Type { get; set; }
public string? Icon { get; set; }
@@ -1,12 +1,12 @@
namespace Umbraco.Cms.Api.Management.ViewModels.TrackedReferences;
using Umbraco.Cms.Api.Management.ViewModels.Document;
public class DocumentReferenceResponseModel : IReferenceResponseModel
namespace Umbraco.Cms.Api.Management.ViewModels.TrackedReferences;
public class DocumentReferenceResponseModel : ReferenceResponseModel
{
public Guid Id { get; set; }
public string? Name { get; set; }
public bool? Published { get; set; }
public TrackedReferenceDocumentType DocumentType { get; set; } = new();
public IEnumerable<DocumentVariantItemResponseModel> Variants { get; set; } = Enumerable.Empty<DocumentVariantItemResponseModel>();
}
@@ -0,0 +1,6 @@
namespace Umbraco.Cms.Api.Management.ViewModels.TrackedReferences;
public class DocumentTypePropertyTypeReferenceResponseModel : ContentTypePropertyTypeReferenceResponseModel
{
public TrackedReferenceDocumentType DocumentType { get; set; } = new();
}
@@ -1,10 +1,6 @@
namespace Umbraco.Cms.Api.Management.ViewModels.TrackedReferences;
namespace Umbraco.Cms.Api.Management.ViewModels.TrackedReferences;
public class MediaReferenceResponseModel : IReferenceResponseModel
public class MediaReferenceResponseModel : ReferenceResponseModel
{
public Guid Id { get; set; }
public string? Name { get; set; }
public TrackedReferenceMediaType MediaType { get; set; } = new();
}
@@ -0,0 +1,6 @@
namespace Umbraco.Cms.Api.Management.ViewModels.TrackedReferences;
public class MediaTypePropertyTypeReferenceResponseModel : ContentTypePropertyTypeReferenceResponseModel
{
public TrackedReferenceMediaType MediaType { get; set; } = new();
}
@@ -0,0 +1,6 @@
namespace Umbraco.Cms.Api.Management.ViewModels.TrackedReferences;
public class MemberReferenceResponseModel : ReferenceResponseModel
{
public TrackedReferenceMemberType MemberType { get; set; } = new();
}
@@ -0,0 +1,6 @@
namespace Umbraco.Cms.Api.Management.ViewModels.TrackedReferences;
public class MemberTypePropertyTypeReferenceResponseModel : ContentTypePropertyTypeReferenceResponseModel
{
public TrackedReferenceMemberType MemberType { get; set; } = new();
}
@@ -0,0 +1,8 @@
namespace Umbraco.Cms.Api.Management.ViewModels.TrackedReferences;
public abstract class ReferenceResponseModel : IReferenceResponseModel
{
public Guid Id { get; set; }
public string? Name { get; set; }
}
@@ -1,7 +1,9 @@
namespace Umbraco.Cms.Api.Management.ViewModels.TrackedReferences;
namespace Umbraco.Cms.Api.Management.ViewModels.TrackedReferences;
public abstract class TrackedReferenceContentType
{
public Guid Id { get; set; }
public string? Icon { get; set; }
public string? Alias { get; set; }
@@ -0,0 +1,5 @@
namespace Umbraco.Cms.Api.Management.ViewModels.TrackedReferences;
public class TrackedReferenceMemberType : TrackedReferenceContentType
{
}
@@ -1,4 +1,3 @@
using Umbraco.Cms.Api.Management.ViewModels.Content;
using Umbraco.Cms.Api.Management.ViewModels.Document;
using Umbraco.Cms.Api.Management.ViewModels.DocumentType;
@@ -8,6 +7,8 @@ public class DocumentTreeItemResponseModel : ContentTreeItemResponseModel
{
public bool IsProtected { get; set; }
public IEnumerable<ReferenceByIdModel> Ancestors { get; set; } = [];
public DocumentTypeReferenceResponseModel DocumentType { get; set; } = new();
public IEnumerable<DocumentVariantItemResponseModel> Variants { get; set; } = Enumerable.Empty<DocumentVariantItemResponseModel>();
@@ -26,23 +26,36 @@
<ProjectReference Include="..\Umbraco.Web.Website\Umbraco.Web.Website.csproj" />
</ItemGroup>
<!-- Restore and build backoffice project -->
<!-- General ignored files -->
<ItemGroup>
<Content Remove="wwwroot\umbraco\assets\README.md" />
</ItemGroup>
<!-- BEGIN: Restore and build backoffice project -->
<PropertyGroup>
<BackofficeProjectDirectory Condition="'$(BackofficeProjectDirectory)' == ''">..\Umbraco.Web.UI.Client\</BackofficeProjectDirectory>
<BackofficeAssetsPath>wwwroot\umbraco\backoffice</BackofficeAssetsPath>
<BackofficeAssetsPath>$(ProjectDir)wwwroot\umbraco\backoffice</BackofficeAssetsPath>
</PropertyGroup>
<ItemGroup>
<BackofficeAssetsInputs Include="$(BackofficeProjectDirectory)package.json;$(BackofficeProjectDirectory)package-lock.json;$(BackofficeProjectDirectory)src\**" Exclude="$(DefaultItemExcludes)" />
<Content Remove="$(BackofficeAssetsPath)\**" />
</ItemGroup>
<Target Name="RestoreBackoffice" Inputs="$(BackofficeProjectDirectory)package-lock.json" Outputs="$(BackofficeProjectDirectory)node_modules\.package-lock.json">
<Message Importance="high" Text="Restoring Backoffice NPM packages..." />
<Exec Command="npm ci --no-fund --no-audit --prefer-offline" WorkingDirectory="$(BackofficeProjectDirectory)" />
<Target Name="BuildStaticAssetsPreconditions" BeforeTargets="AssignTargetPaths">
<Message Text="Skip BuildBackoffice target because UmbracoBuild is '$(UmbracoBuild)' (this is not Visual Studio)" Importance="high" Condition="'$(UmbracoBuild)' != ''" />
<Message Text="Skip BuildBackoffice target because '$(BackofficeAssetsPath)' already exists" Importance="high" Condition="Exists('$(BackofficeAssetsPath)')" />
<Message Text="Call BuildBackoffice target because UmbracoBuild is empty (this is Visual Studio) and '$(BackofficeAssetsPath)' doesn't exist" Importance="high" Condition="'$(UmbracoBuild)' == '' and !Exists('$(BackofficeAssetsPath)')" />
<CallTarget Targets="BuildBackoffice" Condition="'$(UmbracoBuild)' == '' and !Exists('$(BackofficeAssetsPath)')" />
</Target>
<Target Name="BuildBackoffice" DependsOnTargets="RestoreBackoffice" BeforeTargets="AssignTargetPaths" Inputs="@(BackofficeAssetsInputs)" Outputs="$(IntermediateOutputPath)backoffice.complete.txt">
<Target Name="RestoreBackoffice" Inputs="$(BackofficeProjectDirectory)package-lock.json" Outputs="$(BackofficeProjectDirectory)node_modules\.package-lock.json">
<Message Importance="high" Text="Restoring Backoffice NPM packages..." />
<Exec Command="npm i --no-fund --no-audit" WorkingDirectory="$(BackofficeProjectDirectory)" />
</Target>
<Target Name="BuildBackoffice" DependsOnTargets="RestoreBackoffice">
<Message Importance="high" Text="Executing Backoffice NPM build script..." />
<Exec Command="npm run build:for:cms" WorkingDirectory="$(BackofficeProjectDirectory)" />
<ItemGroup>
@@ -61,27 +74,46 @@
</DefineStaticWebAssets>
</Target>
<!-- Restore and build login project -->
<Target Name="CleanStaticAssetsPreconditions" AfterTargets="Clean" Condition="'$(UmbracoBuild)' == ''">
<Message Text="Skip CleanBackoffice target because '$(BackofficeAssetsPath)' doesn't exist" Importance="high" Condition="!Exists('$(BackofficeAssetsPath)')" />
<Message Text="Skip CleanBackoffice target because preserve.backoffice marker file exists" Importance="high" Condition="Exists('$(BackofficeAssetsPath)') and Exists('$(SolutionDir)preserve.backoffice')" />
<Message Text="Call CleanBackoffice target because '$(BackofficeAssetsPath)' exists and preserve.backoffice marker file doesn't exist" Importance="high" Condition="Exists('$(BackofficeAssetsPath)') and !Exists('$(SolutionDir)preserve.backoffice')" />
<CallTarget Targets="CleanBackoffice" Condition="Exists('$(BackofficeAssetsPath)') and !Exists('$(SolutionDir)preserve.backoffice')" />
</Target>
<Target Name="CleanBackoffice">
<ItemGroup>
<BackofficeDirectories Include="$(BackofficeAssetsPath)" />
</ItemGroup>
<RemoveDir Directories="@(BackofficeDirectories)" />
</Target>
<!-- END: Restore and build backoffice project -->
<!-- BEGIN: Restore and build login project -->
<PropertyGroup>
<LoginProjectDirectory Condition="'$(LoginProjectDirectory)' == ''">..\Umbraco.Web.UI.Login\</LoginProjectDirectory>
<LoginAssetsPath>wwwroot\umbraco\login</LoginAssetsPath>
<LoginAssetsPath>$(ProjectDir)wwwroot\umbraco\login</LoginAssetsPath>
</PropertyGroup>
<ItemGroup>
<LoginAssetsInputs Include="$(LoginProjectDirectory)**" Exclude="$(DefaultItemExcludes)" />
<Content Remove="$(LoginAssetsPath)\**" />
</ItemGroup>
<ItemGroup>
<Content Remove="wwwroot\umbraco\assets\README.md" />
</ItemGroup>
<Target Name="BuildLoginStaticAssetsPreconditions" BeforeTargets="AssignTargetPaths">
<Message Text="Skip BuildLogin target because UmbracoBuild is '$(UmbracoBuild)' (this is not Visual Studio)" Importance="high" Condition="'$(UmbracoBuild)' != ''" />
<Message Text="Skip BuildLogin target because '$(LoginAssetsPath)' already exists" Importance="high" Condition="Exists('$(LoginAssetsPath)')" />
<Message Text="Call BuildLogin target because UmbracoBuild is empty (this is Visual Studio) and '$(LoginAssetsPath)' doesn't exist" Importance="high" Condition="'$(UmbracoBuild)' == '' and !Exists('$(LoginAssetsPath)')" />
<CallTarget Targets="BuildLogin" Condition="'$(UmbracoBuild)' == '' and !Exists('$(LoginAssetsPath)')" />
</Target>
<Target Name="RestoreLogin" Inputs="$(LoginProjectDirectory)package-lock.json" Outputs="$(LoginProjectDirectory)node_modules/.package-lock.json">
<Message Importance="high" Text="Restoring Login NPM packages..." />
<Exec Command="npm ci --no-fund --no-audit --prefer-offline" WorkingDirectory="$(LoginProjectDirectory)" />
<Exec Command="npm i --no-fund --no-audit" WorkingDirectory="$(LoginProjectDirectory)" />
</Target>
<Target Name="BuildLogin" DependsOnTargets="RestoreLogin" BeforeTargets="AssignTargetPaths" Inputs="@(LoginAssetsInputs)" Outputs="$(IntermediateOutputPath)login.complete.txt">
<Target Name="BuildLogin" DependsOnTargets="RestoreLogin">
<Message Importance="high" Text="Executing Login NPM build script..." />
<Exec Command="npm run build" WorkingDirectory="$(LoginProjectDirectory)" />
<ItemGroup>
@@ -99,4 +131,19 @@
<Output TaskParameter="Assets" ItemName="StaticWebAsset" />
</DefineStaticWebAssets>
</Target>
<Target Name="CleanLoginStaticAssetsPreconditions" AfterTargets="Clean" Condition="'$(UmbracoBuild)' == ''">
<Message Text="Skip CleanLogin target because '$(LoginAssetsPath)' doesn't exist" Importance="high" Condition="!Exists('$(LoginAssetsPath)')" />
<Message Text="Skip CleanLogin target because preserve.login marker file exists" Importance="high" Condition="Exists('$(LoginAssetsPath)') and Exists('$(SolutionDir)preserve.login')" />
<Message Text="Call CleanLogin target because '$(LoginAssetsPath)' exists and preserve.login marker file doesn't exist" Importance="high" Condition="Exists('$(LoginAssetsPath)') and !Exists('$(SolutionDir)preserve.login')" />
<CallTarget Targets="CleanLogin" Condition="Exists('$(LoginAssetsPath)') and !Exists('$(SolutionDir)preserve.login')" />
</Target>
<Target Name="CleanLogin">
<ItemGroup>
<LoginDirectories Include="$(LoginAssetsPath)" />
</ItemGroup>
<RemoveDir Directories="@(LoginDirectories)" />
</Target>
<!-- END: Restore and build login project -->
</Project>
+2
View File
@@ -37,6 +37,7 @@ public interface IAction : IDiscoverable
/// <summary>
/// Gets the icon to display for this action
/// </summary>
[Obsolete("No longer used. Scheduled for removal in V17.")]
string Icon { get; }
/// <summary>
@@ -51,5 +52,6 @@ public interface IAction : IDiscoverable
/// <remarks>
/// Used in the UI when assigning permissions
/// </remarks>
[Obsolete("No longer used. Scheduled for removal in V17.")]
string? Category { get; }
}
+2
View File
@@ -22,4 +22,6 @@ public static class CacheKeys
public const string PreviewPropertyCacheKeyPrefix = "Cache.Property.CacheValues[D:";
public const string PropertyCacheKeyPrefix = "Cache.Property.CacheValues[P:";
public const string MemberUserNameCachePrefix = "uRepo_userNameKey+";
}
@@ -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)
@@ -71,11 +71,22 @@ public sealed class MemberCacheRefresher : PayloadCacheRefresherBase<MemberCache
foreach (JsonPayload p in payloads)
{
_idKeyMap.ClearCache(p.Id);
if (memberCache.Success)
if (memberCache.Success is false)
{
memberCache.Result?.Clear(RepositoryCacheKeys.GetKey<IMember, int>(p.Id));
memberCache.Result?.Clear(RepositoryCacheKeys.GetKey<IMember, string>(p.Username));
continue;
}
memberCache.Result?.Clear(RepositoryCacheKeys.GetKey<IMember, int>(p.Id));
memberCache.Result?.Clear(RepositoryCacheKeys.GetKey<IMember, string>(p.Username));
// This specific cache key was introduced to fix an issue where the member username could not be the same as the member id, because the cache keys collided.
// This is done in a bit of a hacky way, because the cache key is created internally in the repository, but we need to clear it here.
// Ideally, we want to use a shared way of generating the key between this and the repository.
// Additionally, the RepositoryCacheKeys actually caches the string to avoid re-allocating memory; we would like to also use this in the repository
// See:
// https://github.com/umbraco/Umbraco-CMS/pull/17350
// https://github.com/umbraco/Umbraco-CMS/pull/17815
memberCache.Result?.Clear(RepositoryCacheKeys.GetKey<IMember, string>(CacheKeys.MemberUserNameCachePrefix + p.Username));
}
}
}
@@ -160,9 +160,9 @@ public class ObservableDictionary<TKey, TValue> : ObservableCollection<TValue>,
if (index != Count)
{
foreach (TKey k in Indecies.Keys.Where(k => Indecies[k] >= index).ToList())
foreach (KeyValuePair<TKey, int> largerOrEqualToIndex in Indecies.Where(kvp => kvp.Value >= index))
{
Indecies[k]++;
Indecies[largerOrEqualToIndex.Key] = largerOrEqualToIndex.Value + 1;
}
}
@@ -185,9 +185,9 @@ public class ObservableDictionary<TKey, TValue> : ObservableCollection<TValue>,
Indecies.Remove(key);
foreach (TKey k in Indecies.Keys.Where(k => Indecies[k] > index).ToList())
foreach (KeyValuePair<TKey, int> largerThanIndex in Indecies.Where(kvp => kvp.Value > index))
{
Indecies[k]--;
Indecies[largerThanIndex.Key] = largerThanIndex.Value - 1;
}
}
+1 -4
View File
@@ -233,10 +233,7 @@ public class TypeFinder : ITypeFinder
excludeFromResults = new HashSet<Assembly>();
}
if (exclusionFilter == null)
{
exclusionFilter = new string[] { };
}
exclusionFilter ??= [];
return GetAllAssemblies()
.Where(x => excludeFromResults.Contains(x) == false
@@ -5,14 +5,17 @@ namespace Umbraco.Extensions;
public static class ContentSettingsExtensions
{
/// <summary>
/// Determines if file extension is allowed for upload based on (optional) white list and black list
/// held in settings.
/// Allow upload if extension is whitelisted OR if there is no whitelist and extension is NOT blacklisted.
/// Determines if file extension is allowed for upload based on (optional) allow list and deny list held in settings.
/// Disallowed file extensions are only considered if there are no allowed file extensions.
/// </summary>
public static bool IsFileAllowedForUpload(this ContentSettings contentSettings, string extension) =>
contentSettings.AllowedUploadedFileExtensions.Any(x => x.InvariantEquals(extension)) ||
(contentSettings.AllowedUploadedFileExtensions.Any() == false &&
contentSettings.DisallowedUploadedFileExtensions.Any(x => x.InvariantEquals(extension)) == false);
/// <param name="contentSettings">The content settings.</param>
/// <param name="extension">The file extension.</param>
/// <returns>
/// <c>true</c> if the file extension is allowed for upload; otherwise, <c>false</c>.
/// </returns>
public static bool IsFileAllowedForUpload(this ContentSettings contentSettings, string extension)
=> contentSettings.AllowedUploadedFileExtensions.Any(x => x.InvariantEquals(extension.Trim())) ||
(contentSettings.AllowedUploadedFileExtensions.Any() == false && contentSettings.DisallowedUploadedFileExtensions.Any(x => x.InvariantEquals(extension.Trim())) == false);
/// <summary>
/// Gets the auto-fill configuration for a specified property alias.
+13 -3
View File
@@ -233,17 +233,27 @@ public static partial class Constants
public const string RelatedMediaName = "Related Media";
/// <summary>
/// Alias for default relation type "Related Media"
/// Alias for default relation type "Related Media".
/// </summary>
public const string RelatedMediaAlias = "umbMedia";
/// <summary>
/// Name for default relation type "Related Member".
/// </summary>
public const string RelatedMemberName = "Related Member";
/// <summary>
/// Alias for default relation type "Related Member".
/// </summary>
public const string RelatedMemberAlias = "umbMember";
/// <summary>
/// Name for default relation type "Related Document".
/// </summary>
public const string RelatedDocumentName = "Related Document";
/// <summary>
/// Alias for default relation type "Related Document"
/// Alias for default relation type "Related Document".
/// </summary>
public const string RelatedDocumentAlias = "umbDocument";
@@ -284,7 +294,7 @@ public static partial class Constants
/// Developers should not manually use these relation types since they will all be cleared whenever an entity
/// (content, media or member) is saved since they are auto-populated based on property values.
/// </remarks>
public static string[] AutomaticRelationTypes { get; } = { RelatedMediaAlias, RelatedDocumentAlias };
public static string[] AutomaticRelationTypes { get; } = { RelatedMediaAlias, RelatedMemberAlias, RelatedDocumentAlias };
// TODO: return a list of built in types so we can use that to prevent deletion in the UI
}
@@ -0,0 +1,25 @@
namespace Umbraco.Cms.Core;
public static partial class Constants
{
/// <summary>
/// Defines reference types.
/// </summary>
/// <remarks>
/// Reference types are used to identify the type of entity that is being referenced when exposing references
/// between Umbraco entities.
/// These are used in the management API and backoffice to indicate and warn editors when working with an entity,
/// as to what other entities depend on it.
/// These consist of references managed by Umbraco relations (e.g. document, media and member).
/// But also references that come from schema (e.g. data type usage on content types).
/// </remarks>
public static class ReferenceType
{
public const string Document = UdiEntityType.Document;
public const string Media = UdiEntityType.Media;
public const string Member = UdiEntityType.Member;
public const string DocumentTypePropertyType = "document-type-property-type";
public const string MediaTypePropertyType = "media-type-property-type";
public const string MemberTypePropertyType = "member-type-property-type";
}
}
@@ -5,6 +5,7 @@ using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Core.Models.DeliveryApi;
using Umbraco.Cms.Core.Models.PublishedContent;
using Umbraco.Cms.Core.PublishedCache;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Services.Navigation;
using Umbraco.Extensions;
@@ -19,6 +20,7 @@ public sealed class ApiContentRouteBuilder : IApiContentRouteBuilder
private readonly IPublishedContentCache _contentCache;
private readonly IDocumentNavigationQueryService _navigationQueryService;
private readonly IPublishStatusQueryService _publishStatusQueryService;
private readonly IDocumentUrlService _documentUrlService;
private RequestHandlerSettings _requestSettings;
public ApiContentRouteBuilder(
@@ -29,7 +31,8 @@ public sealed class ApiContentRouteBuilder : IApiContentRouteBuilder
IOptionsMonitor<RequestHandlerSettings> requestSettings,
IPublishedContentCache contentCache,
IDocumentNavigationQueryService navigationQueryService,
IPublishStatusQueryService publishStatusQueryService)
IPublishStatusQueryService publishStatusQueryService,
IDocumentUrlService documentUrlService)
{
_apiContentPathProvider = apiContentPathProvider;
_variationContextAccessor = variationContextAccessor;
@@ -37,11 +40,35 @@ public sealed class ApiContentRouteBuilder : IApiContentRouteBuilder
_contentCache = contentCache;
_navigationQueryService = navigationQueryService;
_publishStatusQueryService = publishStatusQueryService;
_documentUrlService = documentUrlService;
_globalSettings = globalSettings.Value;
_requestSettings = requestSettings.CurrentValue;
requestSettings.OnChange(settings => _requestSettings = settings);
}
[Obsolete("Use the non-obsolete constructor, scheduled for removal in v17")]
public ApiContentRouteBuilder(
IApiContentPathProvider apiContentPathProvider,
IOptions<GlobalSettings> globalSettings,
IVariationContextAccessor variationContextAccessor,
IRequestPreviewService requestPreviewService,
IOptionsMonitor<RequestHandlerSettings> requestSettings,
IPublishedContentCache contentCache,
IDocumentNavigationQueryService navigationQueryService,
IPublishStatusQueryService publishStatusQueryService)
: this(
apiContentPathProvider,
globalSettings,
variationContextAccessor,
requestPreviewService,
requestSettings,
contentCache,
navigationQueryService,
publishStatusQueryService,
StaticServiceProvider.Instance.GetRequiredService<IDocumentUrlService>())
{
}
[Obsolete("Use the non-obsolete constructor, scheduled for removal in v17")]
public ApiContentRouteBuilder(
IApiContentPathProvider apiContentPathProvider,
@@ -59,7 +86,8 @@ public sealed class ApiContentRouteBuilder : IApiContentRouteBuilder
requestSettings,
contentCache,
navigationQueryService,
StaticServiceProvider.Instance.GetRequiredService<IPublishStatusQueryService>())
StaticServiceProvider.Instance.GetRequiredService<IPublishStatusQueryService>(),
StaticServiceProvider.Instance.GetRequiredService<IDocumentUrlService>())
{
}
@@ -113,7 +141,7 @@ public sealed class ApiContentRouteBuilder : IApiContentRouteBuilder
// we can perform fallback to the content route.
if (IsInvalidContentPath(contentPath))
{
contentPath = _contentCache.GetRouteById(content.Id, culture) ?? contentPath;
contentPath = _documentUrlService.GetLegacyRouteFormat(content.Key, culture ?? _variationContextAccessor.VariationContext?.Culture, isPreview);
}
// if the content path has still not been resolved as a valid path, the content is un-routable in this culture
@@ -125,7 +153,9 @@ public sealed class ApiContentRouteBuilder : IApiContentRouteBuilder
: null;
}
return contentPath;
return _requestSettings.AddTrailingSlash
? contentPath?.EnsureEndsWith('/')
: contentPath?.TrimEnd('/');
}
private string ContentPreviewPath(IPublishedContent content) => $"{Constants.DeliveryApi.Routing.PreviewContentPathPrefix}{content.Key:D}{(_requestSettings.AddTrailingSlash ? "/" : string.Empty)}";
@@ -86,10 +86,35 @@ public sealed class ApiPublishedContentCache : IApiPublishedContentCache
_variationContextAccessor.VariationContext?.Culture,
_requestPreviewService.IsPreview());
// in multi-root settings, we've historically resolved all but the first root by their ID + URL segment,
// e.g. "1234/second-root-url-segment". in V15+, IDocumentUrlService won't resolve this anymore; it will
// however resolve "1234/" correctly, so to remain backwards compatible, we need to perform this extra step.
var verifyUrlSegment = false;
if (documentKey is null && route.TrimEnd('/').CountOccurrences("/") is 1)
{
documentKey = _apiDocumentUrlService.GetDocumentKeyByRoute(
route[..(route.IndexOf('/') + 1)],
_variationContextAccessor.VariationContext?.Culture,
_requestPreviewService.IsPreview());
verifyUrlSegment = true;
}
IPublishedContent? content = documentKey.HasValue
? _publishedContentCache.GetById(isPreviewMode, documentKey.Value)
: null;
// the additional look-up above can result in false positives; if attempting to request a non-existing child to
// the currently contextualized request root (either by start item or by domain), the root content key might
// get resolved. to counter for this, we compare the requested URL segment with the resolved content URL segment.
if (content is not null && verifyUrlSegment)
{
var expectedUrlSegment = route[(route.IndexOf('/') + 1)..];
if (content.UrlSegment != expectedUrlSegment)
{
content = null;
}
}
return ContentOrNullIfDisallowed(content);
}
@@ -422,6 +422,7 @@ namespace Umbraco.Cms.Core.DependencyInjection
// Two factor providers
Services.AddUnique<ITwoFactorLoginService, TwoFactorLoginService>();
Services.AddUnique<IUserTwoFactorLoginService, UserTwoFactorLoginService>();
Services.AddUnique<IMemberTwoFactorLoginService, MemberTwoFactorLoginService>();
// Add Query services
Services.AddUnique<IDocumentRecycleBinQueryService, DocumentRecycleBinQueryService>();
@@ -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>
@@ -35,7 +35,7 @@ public class InvalidCompositionException : Exception
/// <param name="addedCompositionAlias">The added composition alias.</param>
/// <param name="propertyTypeAliases">The property type aliases.</param>
public InvalidCompositionException(string contentTypeAlias, string? addedCompositionAlias, string[] propertyTypeAliases)
: this(contentTypeAlias, addedCompositionAlias, propertyTypeAliases, new string[0])
: this(contentTypeAlias, addedCompositionAlias, propertyTypeAliases, [])
{
}
@@ -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;
}
@@ -7,6 +7,9 @@ namespace Umbraco.Extensions;
public static class DateTimeExtensions
{
/// <summary>
/// Defines the levels to truncate a date to.
/// </summary>
public enum DateTruncate
{
Year,
@@ -25,33 +28,39 @@ public static class DateTimeExtensions
public static string ToIsoString(this DateTime dt) =>
dt.ToString("yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture);
/// <summary>
/// Truncates the date to the specified level, i.e. if you pass in DateTruncate.Hour it will truncate the date to the hour.
/// </summary>
/// <param name="dt">The date.</param>
/// <param name="truncateTo">The level to truncate the date to.</param>
/// <returns>The truncated date.</returns>
public static DateTime TruncateTo(this DateTime dt, DateTruncate truncateTo)
{
if (truncateTo == DateTruncate.Year)
{
return new DateTime(dt.Year, 1, 1);
return new DateTime(dt.Year, 1, 1, 0, 0, 0, dt.Kind);
}
if (truncateTo == DateTruncate.Month)
{
return new DateTime(dt.Year, dt.Month, 1);
return new DateTime(dt.Year, dt.Month, 1, 0, 0, 0, dt.Kind);
}
if (truncateTo == DateTruncate.Day)
{
return new DateTime(dt.Year, dt.Month, dt.Day);
return new DateTime(dt.Year, dt.Month, dt.Day, 0, 0, 0, dt.Kind);
}
if (truncateTo == DateTruncate.Hour)
{
return new DateTime(dt.Year, dt.Month, dt.Day, dt.Hour, 0, 0);
return new DateTime(dt.Year, dt.Month, dt.Day, dt.Hour, 0, 0, dt.Kind);
}
if (truncateTo == DateTruncate.Minute)
{
return new DateTime(dt.Year, dt.Month, dt.Day, dt.Hour, dt.Minute, 0);
return new DateTime(dt.Year, dt.Month, dt.Day, dt.Hour, dt.Minute, 0, dt.Kind);
}
return new DateTime(dt.Year, dt.Month, dt.Day, dt.Hour, dt.Minute, dt.Second);
return new DateTime(dt.Year, dt.Month, dt.Day, dt.Hour, dt.Minute, dt.Second, dt.Kind);
}
}
+2 -2
View File
@@ -27,8 +27,8 @@ public static class IntExtensions
/// </returns>
public static Guid ToGuid(this int value)
{
var bytes = new byte[16];
BitConverter.GetBytes(value).CopyTo(bytes, 0);
Span<byte> bytes = stackalloc byte[16];
BitConverter.GetBytes(value).CopyTo(bytes);
return new Guid(bytes);
}
}
@@ -3850,9 +3850,8 @@ public static class PublishedContentExtensions
if (parentKey.HasValue)
{
IEnumerable<Guid> childrenKeys;
var foundChildrenKeys = contentTypeAlias is null
? navigationQueryService.TryGetChildrenKeys(parentKey.Value, out childrenKeys)
? navigationQueryService.TryGetChildrenKeys(parentKey.Value, out IEnumerable<Guid> childrenKeys)
: navigationQueryService.TryGetChildrenKeysOfType(parentKey.Value, contentTypeAlias, out childrenKeys);
return foundChildrenKeys
@@ -3860,19 +3859,12 @@ public static class PublishedContentExtensions
: [];
}
IEnumerable<Guid> rootKeys;
var foundRootKeys = contentTypeAlias is null
? navigationQueryService.TryGetRootKeys(out rootKeys)
? navigationQueryService.TryGetRootKeys(out IEnumerable<Guid> rootKeys)
: navigationQueryService.TryGetRootKeysOfType(contentTypeAlias, out rootKeys);
if (foundRootKeys)
{
IEnumerable<Guid> rootKeysArray = rootKeys as Guid[] ?? rootKeys.ToArray();
return rootKeysArray.Contains(content.Key)
? publishedStatusFilteringService.FilterAvailable(rootKeysArray, culture)
: [];
}
return [];
return foundRootKeys
? publishedStatusFilteringService.FilterAvailable(rootKeys, culture)
: [];
}
}
+26 -22
View File
@@ -5,6 +5,7 @@ using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Runtime.InteropServices;
using System.Security.Cryptography;
using System.Text;
using System.Text.RegularExpressions;
@@ -57,17 +58,24 @@ public static class StringExtensions
/// <returns></returns>
public static int[] GetIdsFromPathReversed(this string path)
{
string[] pathSegments = path.Split(Constants.CharArrays.Comma, StringSplitOptions.RemoveEmptyEntries);
List<int> nodeIds = new(pathSegments.Length);
for (int i = pathSegments.Length - 1; i >= 0; i--)
ReadOnlySpan<char> pathSpan = path.AsSpan();
List<int> nodeIds = [];
foreach (Range rangeOfPathSegment in pathSpan.Split(Constants.CharArrays.Comma))
{
if (int.TryParse(pathSegments[i], NumberStyles.Integer, CultureInfo.InvariantCulture, out int pathSegment))
if (int.TryParse(pathSpan[rangeOfPathSegment], NumberStyles.Integer, CultureInfo.InvariantCulture, out int pathSegment))
{
nodeIds.Add(pathSegment);
}
}
return nodeIds.ToArray();
var result = new int[nodeIds.Count];
var resultIndex = 0;
for (int i = nodeIds.Count - 1; i >= 0; i--)
{
result[resultIndex++] = nodeIds[i];
}
return result;
}
/// <summary>
@@ -152,14 +160,16 @@ public static class StringExtensions
public static string ReplaceNonAlphanumericChars(this string input, char replacement)
{
var inputArray = input.ToCharArray();
var outputArray = new char[input.Length];
for (var i = 0; i < inputArray.Length; i++)
var chars = input.ToCharArray();
for (var i = 0; i < chars.Length; i++)
{
outputArray[i] = char.IsLetterOrDigit(inputArray[i]) ? inputArray[i] : replacement;
if (!char.IsLetterOrDigit(chars[i]))
{
chars[i] = replacement;
}
}
return new string(outputArray);
return new string(chars);
}
/// <summary>
@@ -209,7 +219,7 @@ public static class StringExtensions
var nonEmpty = queryStrings.Where(x => !x.IsNullOrWhiteSpace()).ToArray();
if (url.Contains("?"))
if (url.Contains('?'))
{
return url + string.Join("&", nonEmpty).EnsureStartsWith('&');
}
@@ -692,7 +702,7 @@ public static class StringExtensions
if (input.Length == 0)
{
return Array.Empty<byte>();
return [];
}
// calc array size - must be groups of 4
@@ -807,7 +817,7 @@ public static class StringExtensions
}
// replace chars that would cause problems in URLs
var chArray = new char[pos];
Span<char> chArray = pos <= 1024 ? stackalloc char[pos] : new char[pos];
for (var i = 0; i < pos; i++)
{
var ch = str[i];
@@ -1293,8 +1303,7 @@ public static class StringExtensions
}
// most bytes from the hash are copied straight to the bytes of the new GUID (steps 5-7, 9, 11-12)
var newGuid = new byte[16];
Array.Copy(hash, 0, newGuid, 0, 16);
Span<byte> newGuid = hash.AsSpan()[..16];
// set the four most significant bits (bits 12 through 15) of the time_hi_and_version field to the appropriate 4-bit version number from Section 4.1.3 (step 8)
newGuid[6] = (byte)((newGuid[6] & 0x0F) | (version << 4));
@@ -1308,7 +1317,7 @@ public static class StringExtensions
}
// Converts a GUID (expressed as a byte array) to/from network order (MSB-first).
internal static void SwapByteOrder(byte[] guid)
internal static void SwapByteOrder(Span<byte> guid)
{
SwapBytes(guid, 0, 3);
SwapBytes(guid, 1, 2);
@@ -1316,12 +1325,7 @@ public static class StringExtensions
SwapBytes(guid, 6, 7);
}
private static void SwapBytes(byte[] guid, int left, int right)
{
var temp = guid[left];
guid[left] = guid[right];
guid[right] = temp;
}
private static void SwapBytes(Span<byte> guid, int left, int right) => (guid[left], guid[right]) = (guid[right], guid[left]);
/// <summary>
/// Checks if a given path is a full path including drive letter
+3 -3
View File
@@ -22,7 +22,7 @@ public static class UriExtensions
/// <remarks>Everything else remains unchanged, except for the fragment which is removed.</remarks>
public static Uri Rewrite(this Uri uri, string path)
{
if (path.StartsWith("/") == false)
if (path.StartsWith("/", StringComparison.Ordinal) == false)
{
throw new ArgumentException("Path must start with a slash.", "path");
}
@@ -42,12 +42,12 @@ public static class UriExtensions
/// <remarks>Everything else remains unchanged, except for the fragment which is removed.</remarks>
public static Uri Rewrite(this Uri uri, string path, string query)
{
if (path.StartsWith("/") == false)
if (path.StartsWith("/", StringComparison.Ordinal) == false)
{
throw new ArgumentException("Path must start with a slash.", "path");
}
if (query.Length > 0 && query.StartsWith("?") == false)
if (query.Length > 0 && query.StartsWith("?", StringComparison.Ordinal) == false)
{
throw new ArgumentException("Query must start with a question mark.", "query");
}
+1 -1
View File
@@ -63,7 +63,7 @@ public static class GuidUtils
// a Guid is 3 blocks + 8 bits
// so it turns into a 3*8+2 = 26 chars string
var chars = new char[length];
Span<char> chars = stackalloc char[length];
var i = 0;
var j = 0;
+6 -4
View File
@@ -28,7 +28,8 @@ public static class HexEncoder
public static string Encode(byte[] bytes)
{
var length = bytes.Length;
var chars = new char[length * 2];
int charsLength = length * 2;
Span<char> chars = charsLength <= 1024 ? stackalloc char[charsLength] : new char[charsLength];
var index = 0;
for (var i = 0; i < length; i++)
@@ -38,7 +39,7 @@ public static class HexEncoder
chars[index++] = HexLutLo[byteIndex];
}
return new string(chars, 0, chars.Length);
return new string(chars);
}
/// <summary>
@@ -54,7 +55,8 @@ public static class HexEncoder
public static string Encode(byte[] bytes, char separator, int blockSize, int blockCount)
{
var length = bytes.Length;
var chars = new char[(length * 2) + blockCount];
int charsLength = (length * 2) + blockCount;
Span<char> chars = charsLength <= 1024 ? stackalloc char[charsLength] : new char[charsLength];
var count = 0;
var size = 0;
var index = 0;
@@ -80,6 +82,6 @@ public static class HexEncoder
count++;
}
return new string(chars, 0, chars.Length);
return new string(chars);
}
}
@@ -0,0 +1,20 @@
namespace Umbraco.Cms.Core.HostedServices;
/// <summary>
/// A Background Task Queue, to enqueue tasks for executing in the background.
/// </summary>
/// <remarks>
/// Borrowed from https://docs.microsoft.com/en-us/aspnet/core/fundamentals/host/hosted-services?view=aspnetcore-5.0
/// </remarks>
public interface IBackgroundTaskQueue
{
/// <summary>
/// Enqueue a work item to be executed in the background.
/// </summary>
void QueueBackgroundWorkItem(Func<CancellationToken, Task> workItem);
/// <summary>
/// Dequeue the first item on the queue.
/// </summary>
Task<Func<CancellationToken, Task>?> DequeueAsync(CancellationToken cancellationToken);
}
+1 -1
View File
@@ -358,7 +358,7 @@ namespace Umbraco.Cms.Core.IO
// nothing prevents us to reach the file, security-wise, yet it is outside
// this filesystem's root - throw
throw new UnauthorizedAccessException($"File original: [{originalPath}] full: [{path}] is outside this filesystem's root.");
throw new UnauthorizedAccessException($"Requested path {originalPath} is outside this filesystem's root.");
}
/// <summary>
@@ -4,7 +4,6 @@ public abstract class RasterizedTypeDetector
{
public static byte[]? GetFileHeader(Stream fileStream)
{
fileStream.Seek(0, SeekOrigin.Begin);
var header = new byte[8];
fileStream.Seek(0, SeekOrigin.Begin);
+1 -1
View File
@@ -197,7 +197,7 @@ public class Content : ContentBase, IContent
/// <inheritdoc />
[IgnoreDataMember]
public IEnumerable<string> PublishedCultures => _publishInfos?.Keys ?? Enumerable.Empty<string>();
public IEnumerable<string> PublishedCultures => _publishInfos?.Keys ?? [];
/// <inheritdoc />
public bool IsCulturePublished(string culture)
@@ -1,4 +1,4 @@
namespace Umbraco.Cms.Core.Models.ContentPublishing;
namespace Umbraco.Cms.Core.Models.ContentPublishing;
public sealed class ContentPublishingBranchResult
{
@@ -7,4 +7,6 @@ public sealed class ContentPublishingBranchResult
public IEnumerable<ContentPublishingBranchItemResult> SucceededItems { get; set; } = [];
public IEnumerable<ContentPublishingBranchItemResult> FailedItems { get; set; } = [];
public Guid? AcceptedTaskId { get; init; }
}
@@ -34,6 +34,9 @@ public struct UmbracoEntityReference : IEquatable<UmbracoEntityReference>
case Constants.UdiEntityType.Media:
RelationTypeAlias = Constants.Conventions.RelationTypes.RelatedMediaAlias;
break;
case Constants.UdiEntityType.Member:
RelationTypeAlias = Constants.Conventions.RelationTypes.RelatedMemberAlias;
break;
default:
// No relation type alias convention for this entity type, so leave it empty
RelationTypeAlias = string.Empty;
+4 -4
View File
@@ -53,8 +53,8 @@ public class User : EntityBase, IUser, IProfile
_language = globalSettings.DefaultUILanguage;
_isApproved = true;
_isLockedOut = false;
_startContentIds = new int[] { };
_startMediaIds = new int[] { };
_startContentIds = [];
_startMediaIds = [];
// cannot be null
_rawPasswordValue = string.Empty;
@@ -101,8 +101,8 @@ public class User : EntityBase, IUser, IProfile
_userGroups = new HashSet<IReadOnlyUserGroup>();
_isApproved = true;
_isLockedOut = false;
_startContentIds = new int[] { };
_startMediaIds = new int[] { };
_startContentIds = [];
_startMediaIds = [];
}
/// <summary>
@@ -39,9 +39,6 @@ public sealed class NavigationNode
child.SortOrder = _children.Count;
_children.Add(childKey);
// Update the navigation structure
navigationStructure[childKey] = child;
}
public void RemoveChild(ConcurrentDictionary<Guid, NavigationNode> navigationStructure, Guid childKey)
@@ -53,8 +50,5 @@ public sealed class NavigationNode
_children.Remove(childKey);
child.Parent = null;
// Update the navigation structure
navigationStructure[childKey] = child;
}
}
@@ -0,0 +1,12 @@
namespace Umbraco.Cms.Core.Models;
/// <summary>
/// Definition of relation directions used as a filter when requesting if a given item has relations.
/// </summary>
[Flags]
public enum RelationDirectionFilter
{
Parent = 1,
Child = 2,
Any = Parent | Child
}
+3
View File
@@ -23,6 +23,9 @@ public class RelationItem
[DataMember(Name = "published")]
public bool? NodePublished { get; set; }
[DataMember(Name = "contentTypeKey")]
public Guid ContentTypeKey { get; set; }
[DataMember(Name = "icon")]
public string? ContentTypeIcon { get; set; }
+5 -1
View File
@@ -1,15 +1,19 @@
namespace Umbraco.Cms.Core.Models;
namespace Umbraco.Cms.Core.Models;
public class RelationItemModel
{
public Guid NodeKey { get; set; }
public string? NodeAlias { get; set; }
public string? NodeName { get; set; }
public string? NodeType { get; set; }
public bool? NodePublished { get; set; }
public Guid ContentTypeKey { get; set; }
public string? ContentTypeIcon { get; set; }
public string? ContentTypeAlias { get; set; }
@@ -11,6 +11,7 @@ public static class RelationTypeExtensions
public static bool IsSystemRelationType(this IRelationType relationType) =>
relationType.Alias == Constants.Conventions.RelationTypes.RelatedDocumentAlias
|| relationType.Alias == Constants.Conventions.RelationTypes.RelatedMediaAlias
|| relationType.Alias == Constants.Conventions.RelationTypes.RelatedMemberAlias
|| relationType.Alias == Constants.Conventions.RelationTypes.RelateDocumentOnCopyAlias
|| relationType.Alias == Constants.Conventions.RelationTypes.RelateParentDocumentOnDeleteAlias
|| relationType.Alias == Constants.Conventions.RelationTypes.RelateParentMediaFolderOnDeleteAlias;
@@ -1,5 +1,6 @@
namespace Umbraco.Cms.Core.Models.TemplateQuery;
[Obsolete("No longer used. Scheduled for removal in V17.")]
public class QueryResultModel
{
public string? QueryExpression { get; set; }
@@ -1,5 +1,6 @@
namespace Umbraco.Cms.Core.Models.TemplateQuery;
[Obsolete("No longer used. Scheduled for removal in V17.")]
public class TemplateQueryResult
{
public string? Icon { get; set; }
@@ -0,0 +1,13 @@
using Umbraco.Cms.Core.Events;
using Umbraco.Cms.Core.Models.Membership;
namespace Umbraco.Cms.Core.Notifications;
public class UserPasswordResettingNotification : CancelableObjectNotification<IUser>
{
public UserPasswordResettingNotification(IUser target, EventMessages messages) : base(target, messages)
{
}
public IUser User => Target;
}
@@ -24,6 +24,5 @@ public interface IDataTypeRepository : IReadWriteQueryRepository<int, IDataType>
/// </summary>
/// <param name="id"></param>
/// <returns></returns>
IReadOnlyDictionary<Udi, IEnumerable<string>> FindListViewUsages(int id) => throw new NotImplementedException();
}
@@ -70,6 +70,29 @@ public interface ITrackedReferencesRepository
bool filterMustBeIsDependency,
out long totalRecords);
/// <summary>
/// Gets a paged result of items which are in relation with an item in the recycle bin.
/// </summary>
/// <param name="objectTypeKey">The Umbraco object type that has recycle bin support (currently Document or Media).</param>
/// <param name="skip">The amount of items to skip.</param>
/// <param name="take">The amount of items to take.</param>
/// <param name="filterMustBeIsDependency">
/// A boolean indicating whether to filter only the RelationTypes which are
/// dependencies (isDependency field is set to true).
/// </param>
/// <param name="totalRecords">The total count of the items with reference to the current item.</param>
/// <returns>An enumerable list of <see cref="RelationItem" /> objects.</returns>
IEnumerable<RelationItemModel> GetPagedRelationsForRecycleBin(
Guid objectTypeKey,
long skip,
long take,
bool filterMustBeIsDependency,
out long totalRecords)
{
totalRecords = 0;
return [];
}
[Obsolete("Use overload that takes key instead of id. This will be removed in Umbraco 15.")]
IEnumerable<RelationItemModel> GetPagedRelationsForItem(
int id,

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