Compare commits

..
148 Commits
Author SHA1 Message Date
Niels LyngsøandGitHub 0331cd154b Update 01_bug_report.yml (#18513) 2025-03-24 14:59:10 +01:00
Jacob Overgaard 0f98b8342c Update branches (v16)
Removed v12, added v15 and v16
2025-03-20 10:49:02 +01:00
dependabot[bot]andJacob Overgaard b13f90046d Bump esbuild and vite in /src/Umbraco.Web.UI.Client
Bumps [esbuild](https://github.com/evanw/esbuild) to 0.25.1 and updates ancestor dependency [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite). These dependencies need to be updated together.


Updates `esbuild` from 0.24.2 to 0.25.1
- [Release notes](https://github.com/evanw/esbuild/releases)
- [Changelog](https://github.com/evanw/esbuild/blob/main/CHANGELOG-2024.md)
- [Commits](https://github.com/evanw/esbuild/compare/v0.24.2...v0.25.1)

Updates `vite` from 6.1.0 to 6.2.2
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v6.2.2/packages/vite)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-03-20 10:15:54 +01:00
Jacob Overgaard 304db8c28a Merge branch 'v15/dev' into contrib 2025-03-20 09:42:56 +01:00
dependabot[bot]andJacob Overgaard 723d3be905 Bump esbuild and vite in /src/Umbraco.Web.UI.Login
Bumps [esbuild](https://github.com/evanw/esbuild) to 0.25.1 and updates ancestor dependency [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite). These dependencies need to be updated together.


Updates `esbuild` from 0.24.2 to 0.25.1
- [Release notes](https://github.com/evanw/esbuild/releases)
- [Changelog](https://github.com/evanw/esbuild/blob/main/CHANGELOG-2024.md)
- [Commits](https://github.com/evanw/esbuild/compare/v0.24.2...v0.25.1)

Updates `vite` from 6.1.0 to 6.2.2
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v6.2.2/packages/vite)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-03-20 09:35:07 +01:00
Andy Butlandandkjac 5f37cd3d20 Make preview check for delivery API content case insensitive. (#18731) 2025-03-20 06:56:43 +01:00
Warren BuckleyandGitHub 203cc0e380 Updates dashboard to use uui-box headline property/attribute & place button in header-actions (#18736) 2025-03-19 23:29:56 +00:00
442de9a803 remove open config (#18592)
Co-authored-by: Niels Lyngsø <niels.lyngso@gmail.com>
2025-03-19 20:24:25 +00:00
Mads RasmussenandGitHub eea6b9b2f1 Feature: Language Item Ref (#18521)
* add entity type to interface

* use item-ref in language input

* register ref item
2025-03-19 20:51:25 +01:00
b3b1ce2dcd Bugfix: block culture permissions (#18665)
* always allow copy

* add back readonly check

* render pencil icon if the property is read only

---------

Co-authored-by: Niels Lyngsø <nsl@umbraco.dk>
2025-03-19 19:42:04 +00:00
69b69a5fc1 V15: Change drag and drop icon (#18519)
* register new drag icons

* use drag icon in sort-children-of modal

* update drag icons

* only use the vertical icon style

* Update property-editor-ui-image-crops.element.ts

* rename to grib

* reset formatting for merge conflict

---------

Co-authored-by: Niels Lyngsø <niels.lyngso@gmail.com>
2025-03-19 19:39:18 +00:00
Mads RasmussenandGitHub 605cda9224 Bugfix: Document granular user permission preset (#18651)
* pass fallback permissions to document granular permissions

* set as preset

* clean up
2025-03-19 19:36:12 +00:00
b65ad4e234 Fix focal point drag not working on firefox (#18674)
* fix focal point drag not working on firefox

* prevent reacting to left click

* only left click for drag

---------

Co-authored-by: Lan Nguyen Thuy <lnt@umbraco.dk>
Co-authored-by: Niels Lyngsø <niels.lyngso@gmail.com>
2025-03-19 16:15:34 +00:00
Jacob OvergaardandGitHub 4e680c88bb build(deps): bump @umbraco-ui/uui from 1.13.0-rc.1 to 1.13.0-rc.2 (#18677) 2025-03-19 15:20:09 +00:00
Jacob OvergaardandGitHub e8f991f485 V15: Adds accepted image types for image cropper (#18715)
* fix: adds accepted image types for image cropper

* chore: cleanup imports and types

* feat: adds a loader until all prerequisites have been resolved for image cropper

* feat: adds method to share temp file configuration
2025-03-19 12:36:51 +00:00
leekelleherandJacob Overgaard 66891333a8 Refactored the Tiptap Table toolbar button
to only show the table actions when the table is active,
otherwise show the Insert Table feature.
2025-03-19 13:26:04 +01:00
Lee KelleherandGitHub 044c9d2c87 Tiptap RTE: Table column/row bubble menu (#18708)
* Code formatting

* Icons for Tiptap table row/column actions

* Combined Tiptap exports

Removed extra constant

* Added Table Column Menu

* Added Table Row Menu

Made the bubble menu reusable
2025-03-19 13:25:13 +01:00
Andy ButlandandGitHub 7b0c419ace Adds detail of how many characters you are over when validating text fields for length. (#18722) 2025-03-19 12:02:40 +01:00
Jacob Overgaard 6ec1b9f1e4 build(deps-dev): bump storybook from 8.5.7 to 8.6.7 and run necessary migrations 2025-03-19 10:14:59 +01:00
Andy ButlandandGitHub 18069633b2 Used documented settings for the media seed cache. (#18723) 2025-03-19 09:47:12 +01:00
b24c29c647 V15: Extend search endpoints (#18634)
* Extend content type search endpoint

* Refactor to be able to specify trashed or not

* Simplify and extract into own method

* Fix breaking ctor

* Make non actions in controllers

* Fox up search service

* Add third ctor to avoid errors

* Update query to filter by is element

* Also implement for media

* Minor formatting and clean-up

* Re-introduce (and obsolete) the previous constructor

---------

Co-authored-by: kjac <kja@umbraco.dk>
2025-03-19 09:12:44 +01:00
Andreas ZerbstandGitHub f3f7fcc051 V15 QA added acceptance tests for block list variants (#18654)
* Moved tests

* Added variant tests

* Updated usage of helper

* Added tests

* Cleaned up

* Bumped version and added test command
2025-03-18 11:17:07 +00:00
Andreas ZerbstandGitHub b6fa93edac V15 QA added Tip Tap block variant acceptance tests (#18668)
* Moved tests

* Added variant tests for tiptap

* Updated file name

* Added test command

* Bumped package lock file

* removed npm command

* Bumped test helpers
2025-03-18 10:37:11 +00:00
Andy ButlandandGitHub b07a24ba55 Added clarifying comments to the logic for granular permissions. (#18705) 2025-03-18 11:08:30 +01:00
Andy ButlandandGitHub 42cb9b5c1e Removes one of the two duplicate ContentPublishingNotification publishings. (#18702) 2025-03-18 10:52:32 +01:00
Andreas ZerbstandGitHub 88657c1bb0 V15 QA added acceptance tests for block grid variants (#18658)
* Added block grid variant tests

* Bumped version

* Updated command to run tests on pipeline

* Removed test command

* Fixed comment
2025-03-18 09:35:28 +00:00
Andy ButlandandGitHub e91a25dcb6 Restrict valid API user client IDs to 100 characters. (#18688) 2025-03-18 06:33:24 +01:00
Lee KelleherandGitHub 81e63e7fa9 Tiptap RTE: Added data-mark attributes (#18689)
Tiptap: adds `data-mark` attributes to RTE, toolbar and toolbar items.
2025-03-18 11:22:20 +07:00
Andy ButlandandGitHub 2f9d58fc68 Fixes link for access to composition from document type property outside of group. (#18686) 2025-03-17 11:16:35 +01:00
Niels Lyngsø e9ba06929f Merge branch 'release/15.3' into v15/dev
# Conflicts:
#	version.json
2025-03-17 11:09:18 +01:00
Andy Butland a5da305093 Merge remote-tracking branch 'origin/v13/dev' into v15/dev
# Conflicts:
#	Directory.Packages.props
#	build/azure-pipelines.yml
#	src/Umbraco.Core/EmbeddedResources/Lang/da.xml
#	src/Umbraco.Core/EmbeddedResources/Lang/en.xml
#	src/Umbraco.Core/EmbeddedResources/Lang/en_us.xml
#	src/Umbraco.Core/Models/ContentEditing/ContentSaveAction.cs
#	src/Umbraco.Web.BackOffice/Authorization/ContentPermissionsQueryStringHandler.cs
#	src/Umbraco.Web.BackOffice/Authorization/MediaPermissionsQueryStringHandler.cs
#	src/Umbraco.Web.BackOffice/Authorization/PermissionsQueryStringHandler.cs
#	src/Umbraco.Web.BackOffice/Controllers/ContentController.cs
#	src/Umbraco.Web.BackOffice/Controllers/MediaController.cs
#	src/Umbraco.Web.BackOffice/Filters/ContentSaveValidationAttribute.cs
#	src/Umbraco.Web.UI.Client/lib/tinymce/langs/da.js
#	src/Umbraco.Web.UI.Client/src/common/directives/components/content/edit.controller.js
#	src/Umbraco.Web.UI.Client/src/common/resources/content.resource.js
#	src/Umbraco.Web.UI.Client/src/less/components/users/umb-user-cards.less
#	src/Umbraco.Web.UI.Client/src/views/content/overlays/publishdescendants.controller.js
#	src/Umbraco.Web.UI.Client/src/views/content/overlays/publishdescendants.html
#	src/Umbraco.Web.UI.Client/src/views/users/views/users/users.html
#	tests/Umbraco.Tests.UnitTests/Umbraco.Web.BackOffice/Authorization/ContentPermissionsQueryStringHandlerTests.cs
#	tests/Umbraco.Tests.UnitTests/Umbraco.Web.BackOffice/Authorization/MediaPermissionsQueryStringHandlerTests.cs
#	tests/Umbraco.Tests.UnitTests/Umbraco.Web.BackOffice/Authorization/MediaPermissionsResourceHandlerTests.cs
2025-03-17 09:20:40 +01:00
Andy ButlandandGitHub e1e5f527e4 Updates dependency on Examine to 3.7. (#18676) 2025-03-17 09:13:09 +01:00
Niels Lyngsø f4e0e38824 Merge branch 'contrib' into v15/dev 2025-03-14 10:21:27 +01:00
Niels Lyngsø fa09ce6214 justify tag 2025-03-14 10:20:23 +01:00
Lee KelleherandGitHub dd8fb66300 Content Picker: Filter out invalid entity types (#18660)
* Reverted `elementName` constant

+ code tidy-up

* Filter out values that don't match the configured entity-type

Fixes #18476

* Notifies the user of invalid data/configuration

with an option to remove invalid data.
2025-03-14 09:55:45 +01:00
Lee KelleherandGitHub a02db287ce Tiptap RTE: Text Indent extension + toolbar items (#18672)
* Tiptap: Text Indent extension

* Updates indent manifest icons
2025-03-13 14:52:58 +00:00
e9c97f8c9b Allow multiple URL segments per document (#18603)
* Code tidy - XML header comments, method ordering, warning resolution.

* Add extension method for retrieving all URL segments for a document.

* Cache and persist multiple URL segments per document.

* Allowed segment providers to terminate or allow additional segments.
Set up currently failing integration test for expected routes.

* Resolved cache issue to ensure passing new integration tests.

* Fixed failing integration test.

* Test class naming tidy up.

* Added resolution and persistance of a primary segment, to retain previous behaviour when a single segment is retrieved.

* Further integration tests.

* Resolved backward compatibility of interface.

* Supress amends made to integration tests.

* Aligned naming of integration tests.

* Removed unused using, added XML header comment.

* Throw on missing table in migration.

* Code clean-up.

* Fix multiple enumeration

* Used default on migrated column.

* Use 1 over true for default value.

* Remove unused logger

---------

Co-authored-by: mole <nikolajlauridsen@protonmail.ch>
2025-03-13 12:47:46 +00:00
Lan Nguyen ThuyandJacob Overgaard f016dbd223 Fix issue text overflow when user name is too long 2025-03-13 11:07:57 +01:00
Mathias HelsengrenandJacob Overgaard da5669d9fc Remove the hardcoded color for the text in error message 2025-03-13 11:04:30 +01:00
Jacob Overgaard 74aa3d9676 bump version to 15.4.0-rc 2025-03-13 10:46:43 +01:00
Niels LyngsøandGitHub c671668e45 remove min height and max height configuration options (#18648) 2025-03-12 21:33:37 +01:00
Niels LyngsøandGitHub e337821d0e align search icon vertical (#18657) 2025-03-12 21:23:41 +01:00
Andy Butland b2aa04ae1a Merge branch 'release/13.7.2' into v13/dev
# Conflicts:
#	version.json
2025-03-12 12:18:45 +01:00
Nhu DinhandGitHub d3c6c87378 V15 QA Added acceptance tests for trash reference by (#18627)
* Added tests for Trash content

* Verified the reference headline

* Updated tests

* Added more tests for trash content

* Updated due to test helper changes

* Bumped version

* Maked trash content tests run in the pipeline

* Fixed comments

* Reverted smokeTest command
2025-03-12 09:58:41 +00:00
Niels LyngsøandGitHub b3e192916d correct labels (#18650) 2025-03-12 09:17:20 +00:00
Andreas ZerbstandGitHub dfe100d7fc V15 QA added block grid with areas tests (#18641)
* Started testing block areas

* Added tests

* Added more tests

* Finished tests

* Moved tests

* Skipped tests

* Fixed comments

* Runs the created tests on the pipeline

* Reverted test command
2025-03-12 09:10:38 +00:00
Andreas ZerbstandGitHub 3420748b35 V15 QA reduced amount of browser downloaded on pipeline to reduce download time (#18642)
* Updated pipeline

* Fixed command
2025-03-12 09:40:03 +01:00
Andy ButlandandGitHub e270adc50f Reverts UI updates from publish with desdendants dialog. (#18647) 2025-03-12 08:10:34 +01:00
Andy ButlandandGitHub ce9460161f Reverts UI updates from publish with descendants dialog (15) (#18646)
* Reverts UI updates from publish with descendants dialog.

* Removed unused variable.
2025-03-12 07:59:37 +01:00
Andy Butland fd5b8de533 Bumped version to 13.7.2 2025-03-12 07:25:35 +01:00
Andy Butland e3d9b042c2 Avoids collection was modified issue when flowing identities to the authenticated user's principal. (#18527) 2025-03-12 07:24:50 +01:00
Andy ButlandandGitHub 54931472f8 Corrects obsoletion message on ITwoFactorLoginService. (#18623) 2025-03-11 17:55:33 +00:00
df6a88b4d5 V15: Enable umbraco-extension client to auto-build (#18597)
* feat: adds autobuild of umbraco extensions created by the dotnet template

* adds file extensions to imports to follow the esmodule convention + apply formatting

* build(deps): upgrade hey-api, vite, typescript and generate new api

* chore: formatting

* clean up actions as client is now automatically being built when starting the site

* revert change with UMBRACO_VERSION_FROM_TEMPLATE

* revert if(includeExample)

* use template name

* use template name

* feat: update the way it sets the auth token

* add back in IncludeExample if

* fix: rename allowPackageTelemetry to allowTelemetry because that is what it is actually called

* `.csproj` amends

- Adds `BeforeTargets` to "BuildClient" target
- Adds "package.json" to "RestoreClient" target input
- Removes extra parameters from `npm i` command

---------

Co-authored-by: leekelleher <leekelleher@gmail.com>
2025-03-11 16:14:47 +00:00
Jacob OvergaardandGitHub 2cc9d2cab0 fix: adds back support for legacy AllowPackageTelemetry (#18644)
Due to an unfortunate mistake in the Umbraco.Cms.Targets generated JSON schema, we alluded to supporting `allowPackageTelemetry` in the JSON files, however the backend didn't support this.
2025-03-11 15:51:38 +00:00
Jacob OvergaardandGitHub c0b6317da8 V15: Reduce the size of the package json schema (#18643)
* chore: combine similar commands

* fix: add specific ignore on tinymce

* fix: override the RawEditorOptions types from tinymce

this ensures that we do not have 70k lines of schema in the generated schema

* fix: import only from the extension-registry

this will automatically find all registered UmbExtensionManifest regardless of which package is imported
2025-03-11 15:50:26 +00:00
leekelleherandJacob Overgaard b2f69691d6 Adds validation to Checkbox List property editor 2025-03-11 14:33:34 +01:00
b684bf0a69 Prevents attempt to move an item to itself as a parent (#18550)
* Prevents attempt to move an item to itself as a parent.

* add default tree item type

* use pickable filter

---------

Co-authored-by: Mads Rasmussen <madsr@hey.com>
2025-03-11 13:30:24 +01:00
Lee KelleherandGitHub 434dd50021 Merge pull request #18633 from umbraco/v15/fix-bug/rte-code-editor-modal-pretty-print
Fix bug RTE code editor modal - pretty print not working
2025-03-11 10:55:49 +00:00
Andy ButlandandGitHub 863d3d11c6 Updates sort property and direction together to avoid intermittent failures (#18590)
* Updates sort property and direction together to avoid intermittent failures.

* Removed debug code.
2025-03-11 11:38:14 +01:00
Niels LyngsøandGitHub d906fc346d use change event (#18631) 2025-03-11 09:19:19 +00:00
PhyxionNLandGitHub ced3db8542 Add NuGet badge (#18583) 2025-03-11 10:09:35 +01:00
leekelleherandJacob Overgaard a48d8a4110 Adds validation to Radio Button List property editor
Fixes #18268
2025-03-11 09:34:31 +01:00
leekelleherandJacob Overgaard c1671f4e9b Adds validation to Dropdown List property editor
Fixes #17271
2025-03-11 09:32:35 +01:00
Andy Butland be4d4bef39 Merge branch 'release/15.2.3' into v15/dev
# Conflicts:
#	version.json
2025-03-11 09:27:22 +01:00
Andy Butland f4ae796b30 Merge branch 'v13/dev' of https://github.com/umbraco/Umbraco-CMS into v13/dev 2025-03-11 09:18:25 +01:00
Andy Butland 367d0386b6 Merge branch 'release/13.7.1' into v13/dev
# Conflicts:
#	version.json
2025-03-11 09:18:12 +01:00
Lan Nguyen Thuy 1d48261b7b fix bug rte code editor modal pretty print not working 2025-03-11 13:39:55 +07:00
Andy ButlandandGitHub ebb6a580dc Merge commit from fork
* Tighten management API endpoint access rules.

* Bumped version to 15.2.1.
2025-03-11 05:11:36 +01:00
5b54bed406 Merge commit from fork
* Fixed parsing of node if in content and media permission querystring handlers to retrieve expected value when multiple are provided in the querystring.

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

* Bumped version to 13.6.1.

* Narrow PermissionQueryString parsing to the releveant UmbracoObjectType

* Add missed update from v10

---------

Co-authored-by: Sven Geusens <sge@umbraco.dk>
2025-03-11 05:11:08 +01:00
Nhu DinhandGitHub bc488d78e4 V15 QA Fixed the failing acceptance tests due to UI changes (#18617)
* Fixed the failing tests

* Bumped version

* Fixed can allow bulk trash in the media section

* Fixed notification message
2025-03-10 14:46:22 +00:00
Anders BjernerandMichael Latouche d3b60277d7 Fixed typo in TinyMCE's da.js 2025-03-10 15:38:50 +01:00
a51cbd3ee9 Added description to block list properties hide content editor (#18619)
* Added description to block list properties hide content editor

* Update the description to use localization

---------

Co-authored-by: Oskar kruger <obk@umbraco.dk>
2025-03-10 13:50:28 +00:00
458eb2e52f Tiptap RTE: Anchor extension + toolbar item (#18575)
* Tiptap: Anchor extension

* Corrected import; fixed the build

* Removed icon, mistakenly added to this branch

it was intended for the Character Map feature.

* Updated dialog style

* Scoped the Anchor modal to Tiptap package

Exported modal alias constant.

* Tweaked selected Anchor styling

* Localizes public labels

* Exported Anchor constants correctly

---------

Co-authored-by: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com>
2025-03-10 13:22:57 +00:00
Lee KelleherandGitHub 7ef11621dd Tiptap RTE: Character Map modal + toolbar item (#18577)
* Tiptap: Character Map extension

* Added missing icon! 🤦

* Added missing manifests

for the CharMap modal

* CharMap dialog styles

* Scoped the Character Map modal to Tiptap package

Exported modal alias constant.
2025-03-10 13:40:58 +01:00
Andy Butland 0d066c4ccc Merge branch 'release/15.3' into v15/dev 2025-03-10 12:04:42 +01:00
Andy ButlandandGitHub 4fe168f540 Removed obsolete integration test. (#18604) 2025-03-10 11:19:40 +01:00
Andy Butland ead277ed3e bumped imagesharp to prevent CVE-2025-27598 (#18602)
# Conflicts:
#	Directory.Packages.props
2025-03-09 08:51:18 +01:00
Andy Butland c35d7d18a8 Merge remote-tracking branch 'origin/v13/dev' into v15/dev
# Conflicts:
#	Directory.Packages.props
2025-03-09 08:46:24 +01:00
twoday-rolandkockandAndy Butland 2ec6ff4ebc bumped imagesharp to prevent CVE-2025-27598 (#18602) 2025-03-09 08:43:52 +01:00
twoday-rolandkockandGitHub 78a8d017a2 bumped imagesharp to prevent CVE-2025-27598 (#18602) 2025-03-09 08:41:06 +01:00
Anders ReusandGitHub 50c3756eb0 V15 QA added test for issue 18555 (#18601)
* Added accceptance test for issue 18555

* Bumped version
2025-03-07 13:51:26 +01:00
Sven Geusens c757f9fcb0 Merge remote-tracking branch 'origin/release/15.3' into v15/dev 2025-03-07 09:13:15 +01:00
Niels LyngsøandGitHub c03b1612b6 update UUI (#18593) 2025-03-06 18:55:14 +00:00
Mads RasmussenandGitHub a79095a869 Support root/area allowed types when pasting in Block Grid (#18480)
* poc allowed root types

* optional filter

* wip custom paste action for block grid

* pass filter

* add filter to property action

* add js docs

* add type

* add type

* add type

* implment filter for block grid areas

* use correct types

* use correct types

* clean up

* update root paste

* clean up

* fix grid paste filter

* fix linter

* Update clipboard.property-context.ts
2025-03-06 14:20:47 +01:00
MoleandGitHub 95c892000d V15: Add textbox and text area serverside validation (#18596)
* Add validation to TextOnlyValueEditor

* Fix tests

* Add tests
2025-03-06 12:19:28 +00:00
Andy ButlandandGitHub 7b6cef6083 Added missing translations for dictionary import. (#18551) 2025-03-06 12:40:42 +01:00
Lee KelleherandGitHub 5bf63c2fa8 Tiptap: Block extension config refresh (#18569)
Refreshes the UI after the Block extension
option has been set as enabled/disabled.

Fixes #18541
2025-03-06 11:42:44 +01:00
Lee KelleherandGitHub a939f8ee1c Chore: Tiptap code sweep 2 (#18579)
* Updates "rte-content.css" to share the Macro Holder style

* Tiptap toolbar menu padding

* Removed unnecessary `isActive` for Embedded Media toolbar button

* Adds scroll container to available toolbar item selection

* Toolbar config: adds style for "menu" items

* Adds missing mappings for the TinyMCE toolbar conversion

* Updates toolbar "menu" styling
2025-03-06 11:41:51 +01:00
29b4056f68 Added server side validation for the Multi Url Picker. (#18589)
* Plumbing

* Handle null value

* Add tests

* Fix error found in tests

---------

Co-authored-by: mole <nikolajlauridsen@protonmail.ch>
2025-03-06 09:52:55 +00:00
45ea6a3cfc Server side validation for property editors (multiple text strings) (#18581)
* Server-side validation for multiple text strings property editor.

* Added unit tests for block list min/max server validation.

* Add danish translations

* Add test showing issue

* Fix issue

---------

Co-authored-by: mole <nikolajlauridsen@protonmail.ch>
2025-03-06 09:48:48 +00:00
6d0dd82781 Move database cache rebuild to a background task with polling (#18496)
* 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.

* Update src/Umbraco.Cms.Api.Management/Controllers/PublishedCache/RebuildPublishedCacheController.cs

Co-authored-by: Sven Geusens <sge@umbraco.dk>

* Removed unnecessary obsolete attribute.

---------

Co-authored-by: Sven Geusens <sge@umbraco.dk>
2025-03-06 10:23:00 +01:00
7b422598f9 [V15] Align reserved property endpoints code and usage (#18319)
* Remove/enhance duplicate code

contentTypeEditing Services now use the IReservedFieldNamesService to determine invalid property alias values.

* Let Modelsbuilder define it's reserved property alias names

* Update, add tests + fix false positives

* Removed config check to register modelsbuilder required reserved properties

* Updated unittests regarding removed check of modelsbuilder mode

* Fix merge

---------

Co-authored-by: Sven Geusens <sge@umbraco.dk>
2025-03-06 09:16:00 +01:00
Niels LyngsøandGitHub 52bb32205b JSDocs update (#18586) 2025-03-06 09:15:30 +01:00
Niels Lyngsø 8a68457421 Merge branch 'release/15.3' into v15/dev
# Conflicts:
#	src/Umbraco.Infrastructure/PropertyEditors/MediaPicker3PropertyEditor.cs
#	src/Umbraco.Web.UI.Client/src/packages/documents/document-types/workspace/document-type/document-type-workspace-editor.element.ts
#	src/Umbraco.Web.UI.Client/src/packages/media/media-types/workspace/media-type-workspace-editor.element.ts
#	src/Umbraco.Web.UI.Client/src/packages/members/member-type/workspace/member-type-workspace-editor.element.ts
#	tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/DecimalValueEditorTests.cs
2025-03-05 15:33:48 +01:00
84d5b7aa52 Server side validation for property editors (drop-down and checkbox list) (#18564)
* WIP...

* Localization and unit tests for multiple value property editor validation.

* Localization fixes.

* Clarified XML header comment.

* Add danish translations

---------

Co-authored-by: mole <nikolajlauridsen@protonmail.ch>
2025-03-05 12:17:31 +00:00
Andy Butland 62690194cf Fixed failing unit test following merge from v13/dev. 2025-03-05 11:47:27 +01:00
Markus JohanssonandGitHub 900938edd0 V15: Added uui-box around ungrouped properties during block editing (#18571) (#18572) 2025-03-05 08:44:49 +01:00
Lee KelleherandGitHub 00e7602b55 V15: Resolves "Save and Preview" backoffice refresh (#18568)
* Reverted `elementName` constant

* Ensures the backoffice preview URL has correct slashes
2025-03-05 08:12:54 +01:00
Andy Butland 7a9e1b66d9 Merge remote-tracking branch 'origin/v13/dev' into v15/merge-from-13 2025-03-05 06:46:13 +01:00
1a205d8727 Handle multiline statements in migrations (#18478)
* Handle multiline statements in migrations

* Fixed failing unit tests and incorrect obsoletion.

* Fixed failing integration tests.

* Applied suggestion from code review.

---------

Co-authored-by: Sven Geusens <sge@umbraco.dk>
2025-03-04 17:27:33 +01:00
867d405992 Provide custom required validator for block list and toggle (#18474)
* Provide custom required validator for block list.

* Adds a custom required validator for the toggle to ensure the value provided is true.

* Remove unnecessary usings

* Remove redundant interface and base constructor

* Remove unnecessary interface

---------

Co-authored-by: mole <nikolajlauridsen@protonmail.ch>
2025-03-04 15:25:10 +00:00
a99c581ab5 V15: Add MNTP serverside validation (#18526)
* Add amount validator

* Add ObjectTypeValidator to MNTP

* Move validate startnode to helper method

* Validate allowed type

* Fix tests

* Added some XML header comments and resolved nit-picky warnings.

* Further XML comments.

* Fix null validation case

---------

Co-authored-by: Andy Butland <abutland73@gmail.com>
2025-03-04 16:06:05 +01:00
Mads RasmussenandGitHub 85883cee85 Feature: Has Children Condition (#18161)
* add children to reload translation

* add has children condition + context

* export

* register manifests

* set hasChildren value for tree items

* add condition to document tree item

* add has children condition to sort children of document

* add conditions for media

* add entity trashed event

* dispatch event when entity is trashed

* remove double event listeners

* export action class

* Update default-tree.context.ts

* wip reload tree when entity is trashed

* move into data folder

* clean up listeners

* move manifest

* wip reload root

* clean up

* add recycle bin tree item

* use for media

* pass entity types through manifest + add recycle bin tree item kind

* remove custom implementation for document recycle bin

* use kind

* rename to supportedEntityTypes

* Update recycle-bin-tree-item.context.ts

* clean up

* remove condition

* only show empty recycle bin if it has children

* remove of sort children
2025-03-04 14:22:22 +00:00
0f9c2332e0 Adds unit tests for RequiredValidator. (#18471)
Co-authored-by: Mole <nikolajlauridsen@protonmail.ch>
2025-03-04 13:49:37 +00:00
a7f5b142e9 Fix issue with server validation from dictionary configuration where floating point values can be be accessed as doubles or ints (#18508)
* Fix issue with server validation from dictionary configuration where floating point values can be be accessed as doubles or ints.

* Fixed typo in comment.

---------

Co-authored-by: Mole <nikolajlauridsen@protonmail.ch>
2025-03-04 13:46:54 +00:00
Andy ButlandandGitHub ca6da751f2 Handle create and update validation for media picker. (#18537) 2025-03-04 14:08:08 +01:00
Mads RasmussenandGitHub 5358c0346e remove the word folder in label (#18485) 2025-03-04 12:21:51 +00:00
6d5b6a4553 Server side validation for property editors (colour picker) (#18557)
* Added server-side validation for colour picker.

* Minor refactor.

* Add danish translation

---------

Co-authored-by: mole <nikolajlauridsen@protonmail.ch>
2025-03-04 12:13:37 +00:00
Andy ButlandandGitHub 396b5ea211 Avoids collection was modified issue when flowing identities to the authenticated user's principal. (#18527) 2025-03-04 12:12:29 +01:00
Mads Rasmussen 7e4dda7bab fix block grid custom views 2025-03-04 12:11:01 +01:00
Andy ButlandandGitHub 3cf9099f56 Remove version from models builder generated code header when configured to do so. (#18501) 2025-03-04 11:52:20 +01:00
Andy ButlandandGitHub 92cb355f4d Correct handling of step values of zero. (#18532) 2025-03-04 11:18:23 +01:00
Mads RasmussenandGitHub fb11191fd1 Feature: Content Type Workspace Editor Header Element (#18520)
* add content-type-workspace-editor-header element

* use for document type

* use in media and member type workspaces

* fix type errors
2025-03-04 10:10:57 +01:00
30186e0bdd V15 QA Fixing the failing acceptance tests 03032025 (#18549)
* Fixed Content tests with slider

* Updated Content tests with true/false

* Fixed due to api helper changes

* Updated tests related to the user has no permission due to an issue

* Added .fixme for Content tests with tiptap

* Bumped version

* Fixed tests for renaming a media

* Added .fixme for the failing tests due to the actual issues

---------

Co-authored-by: Andreas Zerbst <73799582+andr317c@users.noreply.github.com>
2025-03-04 09:00:45 +00:00
Jacob OvergaardandGitHub 2b24b40a75 V15: Workspace buttons cannot be overwritten in all cases (#18525)
* fix: consider that overwrites can be strings OR arrays

* chore: formatting

* chore(mock): use correct javascript for mock plugins

* chore: uses intersection helper method to save on code

* fix: should work without requesting extra update to the the extensions controller refreshing the state
2025-03-04 08:54:38 +00:00
Andy ButlandandGitHub 09bcede957 Fixes multiple available constructors exception for execute template query constructor. (#18552) 2025-03-04 09:51:36 +01:00
Chris HoustonandSebastiaan Janssen 998b6f9296 Fixes the Fatal icon text so it's readable in Dark Mode. (#18375) 2025-03-04 09:45:33 +01:00
2826678049 Added option for filtering the content types allowed as children based on the parent node (#18380)
* Added option for filtering the content types allowed as children based on the parent node.

* Updated OpenApi.json.

* Apply suggestions from code review

Co-authored-by: Sven Geusens <sge@umbraco.dk>

---------

Co-authored-by: Sven Geusens <sge@umbraco.dk>
2025-03-04 09:34:17 +01:00
Kenn JacobsenandGitHub 67ed38f6a2 Guard against element type mismatch at document type creation (#18529) 2025-03-04 09:29:29 +01:00
Mads RasmussenandGitHub fbe2cfacd8 set additionalOptions on the rollback entity action (#18489) 2025-03-04 09:18:53 +01:00
Andy ButlandandGitHub e8ff21987b Fixed grammer of unauthorized message. (#18553) 2025-03-04 08:37:09 +01:00
Andy Butland b9250c20e9 Merge branch 'v15/dev' into contrib 2025-03-04 07:43:10 +01:00
Jacob OvergaardandGitHub e9ef05057e V15: Adds migration of data type configuration when changing property editor UI (#18534)
* feat: adds comparer to keep useful data around when migrating from one property editor ui to another

this is really useful when migrating from tinymce to tiptap for example

* stores values in property to access later
2025-03-03 21:22:36 +00:00
Mathias HelsengrenandJacob Overgaard 8379cba15d Changed so the the logviewer's date input is using the umb-input-date instead, gave the umb-input-date a color-scheme option, and added the color-scheme dark, for the darkmode css. 2025-03-03 22:02:39 +01:00
51cce234f4 V15: Allows certain adjustments of the logo (#18499)
* feat: uses the app-logo component to show the large logo

* feat: rename and publish CSS variables to modify the logo

* feat: sets size of logo to 55px to match login screen

* feat: adds css variables to customize the dimensions of the login logo

* fix: uses `ifDefined` in case logo is not defined

* fix: uses the intrinsic logo path from `when()`

* Fixed up import types

* uses the intrinsic alternative logo path from `when()`

* feat: changes logo based on theme

* feat: forces dark theme on the header logo

* feat: adds blue logomark

* feat: adds logo-alternative path to the backend

* chore: minify logos

* feat: optimises logo sizes

---------

Co-authored-by: leekelleher <leekelleher@gmail.com>
2025-03-03 15:54:22 +00:00
865a2cd83a Added tests and localization to radio button validation (#18512)
* Added tests and localization to radio button validation.

* Remove unnecessary ToString

* Add danish translations

---------

Co-authored-by: mole <nikolajlauridsen@protonmail.ch>
2025-03-03 12:23:55 +00:00
Andy ButlandandGitHub 33d83e1491 Added obsoletion messages for unused interface and implementation for cache rebuilds (#18524) 2025-03-03 13:16:23 +01:00
Andy ButlandandGitHub f6b4ddf598 Localize the email property editor validation and add tests (#18461)
* Localize the email property editor validation and add tests.

* Reverted trim to ensure behaviour for whitespace is unchanged.
2025-03-03 13:13:50 +01:00
Nhu DinhandGitHub 2ebc6badd7 V15 QA Publish E2E test results in the Azure pipeline (#18498)
* Updated playwright config to export the Junit report

* Published the Junit report

* Fixed

* Updated outputDir

* Make one test failed

* Changed npm command

* Fixed outputFile

* Updated folder to export test result

* Make another tests failed

* Updated publish test results job

* Changed testSQLite command

* Updated testRunTitle

* Changed npm command

* Updated testRunTitle

* Reverted

* Fixed comment

* Make some tests failed to test

* Reverted
2025-03-03 13:11:07 +07:00
Mads Rasmussen 2bb894c038 15.3: Hotfix: Content type discard changes (#18490)
* wrap in entity detail workspace element

* use element on media type and member type

* add developer console warning

* sync current data with owner content type

* Update content-type-workspace-context-base.ts

* fix lint error
2025-02-28 20:15:47 +01:00
Lee KelleherandGitHub 3ef9d64f1a Tiptap RTE: Stylesheets (#18502)
* Copies the "TinyMCE Stylesheets Property Editor UI" to the Templating/Stylesheet package

* Deprecates the "TinyMCE Stylesheets Property Editor UI"

as it may be used in 3rd party extensions.

TinyMCE will be removed in v16, but this deprecation notice in 15.4
will at least notify that the "Umb.PropertyEditorUi.StylesheetPicker"
UI can be used.

* Updates TinyMCE to use the Stylesheet Picker UI

* Prevent Stylesheet folders from being picked

+ UI tweaks

* Updates Stylesheet mock data

* Stylesheet Rule Input - removes Edit button

uses `@open` event for UI consistency with other pickers.

* Updates pangram

* Amending to be "header" instead of "heading"

* Adds "stylesheets" config to Tiptap data-type

* Updates RTE styles for Tiptap

Moves "StarterKit" styles to the RTE Essentials extension registration.

Scopes TinyMCE in "rte-content.css" (for now).
2025-02-28 15:37:52 +00:00
Andy ButlandandJacob Overgaard a20f11e48f Makes a few labels friendlier for screen readers. 2025-02-28 16:07:56 +01:00
Mads RasmussenandGitHub 4795609973 Removet the Tip tap placeholder text (#18486) 2025-02-28 14:01:54 +01:00
Mads RasmussenandGitHub 8e7809d37e align tags (#18487) 2025-02-28 13:56:05 +01:00
Mads RasmussenandGitHub 96febee559 add mandatory attribute (#18488) 2025-02-28 13:55:06 +01:00
Andy Butland 82eeb0737b Merge remote-tracking branch 'origin/v13/dev' into v15/merge-from-13
# Conflicts:
#	src/Umbraco.Core/EmbeddedResources/Lang/da.xml
#	src/Umbraco.Core/EmbeddedResources/Lang/en.xml
#	src/Umbraco.Core/EmbeddedResources/Lang/en_us.xml
#	src/Umbraco.Web.BackOffice/Controllers/ContentController.cs
#	src/Umbraco.Web.UI.Client/src/views/content/overlays/publishdescendants.html
2025-02-28 07:40:43 +01:00
Andy Butland 451b5f96e7 Use windows for build agent to avoid hanging issues with Linux. 2025-02-28 06:57:35 +01:00
Andy Butland 15ebae5025 Bumped version to 13.7.1. 2025-02-28 06:57:00 +01:00
Andy Butland 64024e418f Bumped version to 15.2.3. 2025-02-28 06:48:13 +01:00
Nhu DinhandGitHub a7a6eb8af6 V15 QA Fixing the failing acceptance tests in the nightly build (#18466)
* Updated permission name due to UI changes

* Added skip to flaky tests

* Add skip to flaky tests

* Bumped version

* Updated content tests due to test helper changes
2025-02-27 11:44:39 +00:00
Lee KelleherandGitHub f6ab6eea75 Tiptap RTE: Reusable toolbar menu component (#18483)
* Adds Tiptap Toolbar Menu extension kind

a reusable menu component.

Removes the `unique` property for menu-items.

* Implements Font Family as toolbar menu kind

* Implements Font Size as toolbar menu kind

* Implements Style Select as toolbar menu kind

* Implements Table as toolbar menu kind

* Markup amends

* Mock data RTE content addition

* "TextDirection" manifest correction

* Text Align: made to be toggleable
2025-02-27 09:52:44 +00:00
Lee KelleherandGitHub 9809db4179 Tiptap: Text color + background color (#18482)
adds a reusable "color picker button" kind extension type.
2025-02-27 09:42:25 +01:00
Andy Butland 32bfbadb8e Merge branch 'release/13.7.0' into v13/dev
# Conflicts:
#	version.json
2025-02-27 06:47:15 +01:00
Kenn JacobsenandGitHub 5ed09ebefa Backport ShowUnroutableContentWarnings to V13 (#18479) 2025-02-26 20:13:27 +01:00
Andy Butland eaea7a67ba Bump version to 13.7.0. 2025-02-26 08:14:26 +01:00
Andy ButlandandGitHub de2114b8c5 Adds warning to publish descendants dialog when force re-publish is selected. (#18409) 2025-02-22 09:39:58 +01:00
Andy ButlandandGitHub 64607ddbaa Disable webhook firing if disable in configuration. (#18383) 2025-02-19 12:17:04 +01:00
Chris HoustonandGitHub ac3b17b390 Fixes the Fatal icon text so it's readable in Dark Mode. (#18375) 2025-02-18 22:00:19 +01:00
510 changed files with 11552 additions and 6076 deletions
+2 -2
View File
@@ -6,8 +6,8 @@ body:
- type: input
id: "version"
attributes:
label: "Which Umbraco version are you using? (Please write the *exact* version, example: 10.1.0)"
description: "Use the help icon in the Umbraco backoffice to find the version you're using"
label: "Which Umbraco version are you using?"
description: "Please write the *exact* version, example: `10.1.0`. Use the help icon in the Umbraco backoffice to find the version you're using"
validations:
required: true
- type: textarea
+5 -4
View File
@@ -1,11 +1,12 @@
# [Umbraco CMS](https://umbraco.com)
[![GitHub license](https://img.shields.io/badge/license-MIT-blue.svg)](../LICENSE.md)
[![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](CONTRIBUTING.md)
![Mastodon Follow](https://img.shields.io/mastodon/follow/110661369750014952?domain=https%3A%2F%2Fumbracocommunity.social)
[![Chat about Umbraco on Discord](https://img.shields.io/discord/869656431308189746?logo=discord&logoColor=fff)](https://discord.gg/umbraco)
[![Read what's going on in the Umbraco Discord chat now](https://img.shields.io/badge/read-discord-blue)](https://discord-chats.umbraco.com)
[![NuGet Version](https://img.shields.io/nuget/v/Umbraco.Cms)](https://www.nuget.org/packages/Umbraco.Cms)
[![Build status](https://img.shields.io/azure-devops/build/umbraco/Umbraco%2520Cms/301?logo=azurepipelines&label=Azure%20Pipelines)](https://umbraco.visualstudio.com/Umbraco%20Cms/_build?definitionId=301)
[![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](CONTRIBUTING.md)
[![Chat about Umbraco on Discord](https://img.shields.io/discord/869656431308189746?logo=discord&logoColor=fff)](https://discord.gg/umbraco)
[![Read what's going on in the Umbraco Discord chat now](https://img.shields.io/badge/read-discord-blue)](https://discord-chats.umbraco.com)
![Mastodon Follow](https://img.shields.io/mastodon/follow/110661369750014952?domain=https%3A%2F%2Fumbracocommunity.social)
### Umbraco is a free and open source .NET content management system. Our mission is to help you deliver delightful digital experiences by making Umbraco friendly, simpler and social.
+2 -2
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.6.0" />
<PackageVersion Include="Examine.Core" Version="3.6.0" />
<PackageVersion Include="Examine" Version="3.7.0" />
<PackageVersion Include="Examine.Core" Version="3.7.0" />
<PackageVersion Include="HtmlAgilityPack" Version="1.11.74" />
<PackageVersion Include="JsonPatch.Net" Version="3.1.1" />
<PackageVersion Include="K4os.Compression.LZ4" Version="1.3.8" />
+28 -6
View File
@@ -593,8 +593,8 @@ stages:
workingDirectory: tests/Umbraco.Tests.AcceptanceTest
# Install Playwright and dependencies
- pwsh: npx playwright install --with-deps
displayName: Install Playwright
- pwsh: npx playwright install chromium
displayName: Install Playwright only with Chromium browser
workingDirectory: tests/Umbraco.Tests.AcceptanceTest
# Test
@@ -623,13 +623,24 @@ stages:
displayName: Copy Playwright results
condition: succeededOrFailed()
# Publish
# Publish test artifacts
- task: PublishPipelineArtifact@1
displayName: Publish test artifacts
condition: succeededOrFailed()
inputs:
targetPath: $(Build.ArtifactStagingDirectory)
artifact: "Acceptance Test Results - $(Agent.JobName) - Attempt #$(System.JobAttempt)"
# Publish test results
- task: PublishTestResults@2
displayName: "Publish test results"
condition: succeededOrFailed()
inputs:
testResultsFormat: 'JUnit'
testResultsFiles: '*.xml'
searchFolder: "tests/Umbraco.Tests.AcceptanceTest/results"
testRunTitle: "$(Agent.JobName)"
- job:
displayName: E2E Tests (SQL Server)
variables:
@@ -749,8 +760,8 @@ stages:
workingDirectory: tests/Umbraco.Tests.AcceptanceTest
# Install Playwright and dependencies
- pwsh: npx playwright install --with-deps
displayName: Install Playwright
- pwsh: npx playwright install chromium
displayName: Install Playwright only with Chromium browser
workingDirectory: tests/Umbraco.Tests.AcceptanceTest
# Test
@@ -788,13 +799,24 @@ stages:
displayName: Copy Playwright results
condition: succeededOrFailed()
# Publish
# Publish test artifacts
- task: PublishPipelineArtifact@1
displayName: Publish test artifacts
condition: succeededOrFailed()
inputs:
targetPath: $(Build.ArtifactStagingDirectory)
artifact: "Acceptance Test Results - $(Agent.JobName) - Attempt #$(System.JobAttempt)"
# Publish test results
- task: PublishTestResults@2
displayName: "Publish test results"
condition: succeededOrFailed()
inputs:
testResultsFormat: 'JUnit'
testResultsFiles: '*.xml'
searchFolder: "tests/Umbraco.Tests.AcceptanceTest/results"
testRunTitle: "$(Agent.JobName)"
###############################################
## Release
###############################################
+20
View File
@@ -248,6 +248,16 @@ stages:
targetPath: $(Build.ArtifactStagingDirectory)
artifact: "Acceptance Test Results - $(Agent.JobName) - Attempt #$(System.JobAttempt)"
# Publish test results
- task: PublishTestResults@2
displayName: "Publish test results"
condition: succeededOrFailed()
inputs:
testResultsFormat: 'JUnit'
testResultsFiles: '*.xml'
searchFolder: "tests/Umbraco.Tests.AcceptanceTest/results"
testRunTitle: "$(Agent.JobName)"
- job:
displayName: E2E Tests (SQL Server)
timeoutInMinutes: 180
@@ -418,3 +428,13 @@ stages:
inputs:
targetPath: $(Build.ArtifactStagingDirectory)
artifact: "Acceptance Test Results - $(Agent.JobName) - Attempt #$(System.JobAttempt)"
# Publish test results
- task: PublishTestResults@2
displayName: "Publish test results"
condition: succeededOrFailed()
inputs:
testResultsFormat: 'JUnit'
testResultsFiles: '*.xml'
searchFolder: "tests/Umbraco.Tests.AcceptanceTest/results"
testRunTitle: "$(Agent.JobName)"
+2 -1
View File
@@ -9,9 +9,10 @@ schedules:
branches:
include:
- v10/dev
- v12/dev
- v13/dev
- v14/dev
- v15/dev
- v16/dev
steps:
- checkout: none
@@ -1,4 +1,4 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http;
using Umbraco.Cms.Core.DeliveryApi;
namespace Umbraco.Cms.Api.Delivery.Services;
@@ -11,5 +11,5 @@ internal sealed class RequestPreviewService : RequestHeaderHandler, IRequestPrev
}
/// <inheritdoc />
public bool IsPreview() => GetHeaderValue("Preview") == "true";
public bool IsPreview() => string.Equals(GetHeaderValue("Preview"), "true", StringComparison.OrdinalIgnoreCase);
}
@@ -30,14 +30,33 @@ public class SearchDocumentItemController : DocumentItemControllerBase
[NonAction]
[Obsolete("Scheduled to be removed in v16, use the non obsoleted method instead")]
public async Task<IActionResult> SearchFromParent(CancellationToken cancellationToken, string query, int skip = 0, int take = 100, Guid? parentId = null)
=> await SearchFromParentWithAllowedTypes(cancellationToken, query, skip, take, parentId);
=> await SearchWithTrashed(cancellationToken, query, null, skip, take, parentId);
[NonAction]
[Obsolete("Scheduled to be removed in v16, use the non obsoleted method instead")]
[ProducesResponseType(typeof(PagedModel<DocumentItemResponseModel>), StatusCodes.Status200OK)]
public async Task<IActionResult> SearchFromParentWithAllowedTypes(
CancellationToken cancellationToken,
string query,
int skip = 0,
int take = 100,
Guid? parentId = null,
[FromQuery] IEnumerable<Guid>? allowedDocumentTypes = null) =>
await SearchWithTrashed(cancellationToken, query, null, skip, take, parentId, allowedDocumentTypes);
[HttpGet("search")]
[MapToApiVersion("1.0")]
[ProducesResponseType(typeof(PagedModel<DocumentItemResponseModel>), StatusCodes.Status200OK)]
public async Task<IActionResult> SearchFromParentWithAllowedTypes(CancellationToken cancellationToken, string query, int skip = 0, int take = 100, Guid? parentId = null, [FromQuery]IEnumerable<Guid>? allowedDocumentTypes = null)
public async Task<IActionResult> SearchWithTrashed(
CancellationToken cancellationToken,
string query,
bool? trashed = null,
int skip = 0,
int take = 100,
Guid? parentId = null,
[FromQuery] IEnumerable<Guid>? allowedDocumentTypes = null)
{
PagedModel<IEntitySlim> searchResult = _indexedEntitySearchService.Search(UmbracoObjectTypes.Document, query, parentId, allowedDocumentTypes, skip, take);
PagedModel<IEntitySlim> searchResult = _indexedEntitySearchService.Search(UmbracoObjectTypes.Document, query, parentId, allowedDocumentTypes, trashed, skip, take);
var result = new PagedModel<DocumentItemResponseModel>
{
Items = searchResult.Items.OfType<IDocumentEntitySlim>().Select(_documentPresentationFactory.CreateItemResponseModel),
@@ -1,5 +1,4 @@
using Asp.Versioning;
using Microsoft.AspNetCore.Authorization;
using Asp.Versioning;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Api.Common.ViewModels.Pagination;
@@ -9,7 +8,6 @@ using Umbraco.Cms.Core.Mapping;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Services.OperationStatus;
using Umbraco.Cms.Web.Common.Authorization;
namespace Umbraco.Cms.Api.Management.Controllers.DocumentType;
@@ -25,6 +23,15 @@ public class AllowedChildrenDocumentTypeController : DocumentTypeControllerBase
_umbracoMapper = umbracoMapper;
}
[NonAction]
[Obsolete("Use the non obsoleted method instead. Scheduled to be removed in v16")]
public async Task<IActionResult> AllowedChildrenByKey(
CancellationToken cancellationToken,
Guid id,
int skip = 0,
int take = 100)
=> await AllowedChildrenByKey(cancellationToken, id, null, skip, take);
[HttpGet("{id:guid}/allowed-children")]
[MapToApiVersion("1.0")]
[ProducesResponseType(typeof(PagedViewModel<AllowedDocumentType>), StatusCodes.Status200OK)]
@@ -32,10 +39,11 @@ public class AllowedChildrenDocumentTypeController : DocumentTypeControllerBase
public async Task<IActionResult> AllowedChildrenByKey(
CancellationToken cancellationToken,
Guid id,
Guid? parentContentKey = null,
int skip = 0,
int take = 100)
{
Attempt<PagedModel<IContentType>?, ContentTypeOperationStatus> attempt = await _contentTypeService.GetAllowedChildrenAsync(id, skip, take);
Attempt<PagedModel<IContentType>?, ContentTypeOperationStatus> attempt = await _contentTypeService.GetAllowedChildrenAsync(id, parentContentKey, skip, take);
if (attempt.Success is false)
{
return OperationStatusResult(attempt.Status);
@@ -1,45 +1,60 @@
using Asp.Versioning;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.DependencyInjection;
using Umbraco.Cms.Api.Management.ViewModels.DocumentType.Item;
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.Services;
using Umbraco.Extensions;
namespace Umbraco.Cms.Api.Management.Controllers.DocumentType.Item;
[ApiVersion("1.0")]
public class SearchDocumentTypeItemController : DocumentTypeItemControllerBase
{
private readonly IEntitySearchService _entitySearchService;
private readonly IContentTypeService _contentTypeService;
private readonly IUmbracoMapper _mapper;
private readonly IContentTypeSearchService _contentTypeSearchService;
[Obsolete("Please use ctor that only accepts IUmbracoMapper & IContentTypeSearchService, scheduled for removal in v17")]
public SearchDocumentTypeItemController(IEntitySearchService entitySearchService, IContentTypeService contentTypeService, IUmbracoMapper mapper)
: this(mapper, StaticServiceProvider.Instance.GetRequiredService<IContentTypeSearchService>())
{
_entitySearchService = entitySearchService;
_contentTypeService = contentTypeService;
_mapper = mapper;
}
[Obsolete("Please use ctor that only accepts IUmbracoMapper & IContentTypeSearchService, scheduled for removal in v17")]
// We need to have this constructor, or else we get ambiguous constructor error
public SearchDocumentTypeItemController(
IEntitySearchService entitySearchService,
IContentTypeService contentTypeService,
IUmbracoMapper mapper,
IContentTypeSearchService contentTypeSearchService)
: this(mapper, contentTypeSearchService)
{
}
[ActivatorUtilitiesConstructor]
public SearchDocumentTypeItemController(IUmbracoMapper mapper, IContentTypeSearchService contentTypeSearchService)
{
_mapper = mapper;
_contentTypeSearchService = contentTypeSearchService;
}
[NonAction]
[Obsolete("Scheduled to be removed in v16, use the non obsoleted method instead")]
public async Task<IActionResult> Search(CancellationToken cancellationToken, string query, int skip = 0, int take = 100)
=> await SearchDocumentType(cancellationToken, query, null, skip, take);
[HttpGet("search")]
[MapToApiVersion("1.0")]
[ProducesResponseType(typeof(PagedModel<DocumentTypeItemResponseModel>), StatusCodes.Status200OK)]
public async Task<IActionResult> Search(CancellationToken cancellationToken, string query, int skip = 0, int take = 100)
public async Task<IActionResult> SearchDocumentType(CancellationToken cancellationToken, string query, bool? isElement = null, int skip = 0, int take = 100)
{
PagedModel<IEntitySlim> searchResult = _entitySearchService.Search(UmbracoObjectTypes.DocumentType, query, skip, take);
if (searchResult.Items.Any() is false)
{
return await Task.FromResult(Ok(new PagedModel<DocumentTypeItemResponseModel> { Total = searchResult.Total }));
}
IEnumerable<IContentType> contentTypes = _contentTypeService.GetMany(searchResult.Items.Select(item => item.Key).ToArray().EmptyNull());
PagedModel<IContentType> contentTypes = await _contentTypeSearchService.SearchAsync(query, isElement, cancellationToken, skip, take);
var result = new PagedModel<DocumentTypeItemResponseModel>
{
Items = _mapper.MapEnumerable<IContentType, DocumentTypeItemResponseModel>(contentTypes),
Total = searchResult.Total
Items = _mapper.MapEnumerable<IContentType, DocumentTypeItemResponseModel>(contentTypes.Items),
Total = contentTypes.Total
};
return Ok(result);
@@ -32,12 +32,17 @@ public class SearchMediaItemController : MediaItemControllerBase
public async Task<IActionResult> SearchFromParent(CancellationToken cancellationToken, string query, int skip = 0, int take = 100, Guid? parentId = null)
=> await SearchFromParentWithAllowedTypes(cancellationToken, query, skip, take, parentId);
[NonAction]
[Obsolete("Scheduled to be removed in v16, use the non obsoleted method instead")]
public async Task<IActionResult> SearchFromParentWithAllowedTypes(CancellationToken cancellationToken, string query, int skip = 0, int take = 100, Guid? parentId = null, [FromQuery]IEnumerable<Guid>? allowedMediaTypes = null)
=> await SearchFromParentWithAllowedTypes(cancellationToken, query, null, skip, take, parentId);
[HttpGet("search")]
[MapToApiVersion("1.0")]
[ProducesResponseType(typeof(PagedModel<MediaItemResponseModel>), StatusCodes.Status200OK)]
public async Task<IActionResult> SearchFromParentWithAllowedTypes(CancellationToken cancellationToken, string query, int skip = 0, int take = 100, Guid? parentId = null, [FromQuery]IEnumerable<Guid>? allowedMediaTypes = null)
public async Task<IActionResult> SearchFromParentWithAllowedTypes(CancellationToken cancellationToken, string query, bool? trashed = null, int skip = 0, int take = 100, Guid? parentId = null, [FromQuery]IEnumerable<Guid>? allowedMediaTypes = null)
{
PagedModel<IEntitySlim> searchResult = _indexedEntitySearchService.Search(UmbracoObjectTypes.Media, query, parentId, allowedMediaTypes, skip, take);
PagedModel<IEntitySlim> searchResult = _indexedEntitySearchService.Search(UmbracoObjectTypes.Media, query, parentId, allowedMediaTypes, trashed, skip, take);
var result = new PagedModel<MediaItemResponseModel>
{
Items = searchResult.Items.OfType<IMediaEntitySlim>().Select(_mediaPresentationFactory.CreateItemResponseModel),
@@ -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;
@@ -23,6 +23,15 @@ public class AllowedChildrenMediaTypeController : MediaTypeControllerBase
_umbracoMapper = umbracoMapper;
}
[NonAction]
[Obsolete("Use the non obsoleted method instead. Scheduled for removal in Umbraco 16.")]
public async Task<IActionResult> AllowedChildrenByKey(
CancellationToken cancellationToken,
Guid id,
int skip = 0,
int take = 100)
=> await AllowedChildrenByKey(cancellationToken, id, null, skip, take);
[HttpGet("{id:guid}/allowed-children")]
[MapToApiVersion("1.0")]
[ProducesResponseType(typeof(PagedViewModel<AllowedMediaType>), StatusCodes.Status200OK)]
@@ -30,10 +39,11 @@ public class AllowedChildrenMediaTypeController : MediaTypeControllerBase
public async Task<IActionResult> AllowedChildrenByKey(
CancellationToken cancellationToken,
Guid id,
Guid? parentContentKey = null,
int skip = 0,
int take = 100)
{
Attempt<PagedModel<IMediaType>?, ContentTypeOperationStatus> attempt = await _mediaTypeService.GetAllowedChildrenAsync(id, skip, take);
Attempt<PagedModel<IMediaType>?, ContentTypeOperationStatus> attempt = await _mediaTypeService.GetAllowedChildrenAsync(id, parentContentKey, skip, take);
if (attempt.Success is false)
{
return OperationStatusResult(attempt.Status);
@@ -1,4 +1,4 @@
using Asp.Versioning;
using Asp.Versioning;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Core.PublishedCache;
@@ -17,7 +17,20 @@ public class RebuildPublishedCacheController : PublishedCacheControllerBase
[ProducesResponseType(StatusCodes.Status200OK)]
public async Task<IActionResult> Rebuild(CancellationToken cancellationToken)
{
_databaseCacheRebuilder.Rebuild();
if (_databaseCacheRebuilder.IsRebuilding())
{
var problemDetails = new ProblemDetails
{
Title = "Database cache can not be rebuilt",
Detail = $"The database cache is in the process of rebuilding.",
Status = StatusCodes.Status400BadRequest,
Type = "Error",
};
return await Task.FromResult(Conflict(problemDetails));
}
_databaseCacheRebuilder.Rebuild(true);
return await Task.FromResult(Ok());
}
}
@@ -0,0 +1,27 @@
using Asp.Versioning;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Api.Management.ViewModels.PublishedCache;
using Umbraco.Cms.Core.PublishedCache;
namespace Umbraco.Cms.Api.Management.Controllers.PublishedCache;
[ApiVersion("1.0")]
public class RebuildPublishedCacheStatusController : PublishedCacheControllerBase
{
private readonly IDatabaseCacheRebuilder _databaseCacheRebuilder;
public RebuildPublishedCacheStatusController(IDatabaseCacheRebuilder databaseCacheRebuilder) => _databaseCacheRebuilder = databaseCacheRebuilder;
[HttpGet("rebuild/status")]
[MapToApiVersion("1.0")]
[ProducesResponseType(typeof(RebuildStatusModel), StatusCodes.Status200OK)]
public Task<IActionResult> Status(CancellationToken cancellationToken)
{
var isRebuilding = _databaseCacheRebuilder.IsRebuilding();
return Task.FromResult((IActionResult)Ok(new RebuildStatusModel
{
IsRebuilding = isRebuilding
}));
}
}
@@ -19,6 +19,7 @@ namespace Umbraco.Cms.Api.Management.Controllers.Security;
public class BackOfficeGraphicsController : Controller
{
public const string LogoRouteName = nameof(BackOfficeGraphicsController) + "." + nameof(Logo);
public const string LogoAlternativeRouteName = nameof(BackOfficeGraphicsController) + "." + nameof(LogoAlternative);
public const string LoginBackGroundRouteName = nameof(BackOfficeGraphicsController) + "." + nameof(LoginBackground);
public const string LoginLogoRouteName = nameof(BackOfficeGraphicsController) + "." + nameof(LoginLogo);
public const string LoginLogoAlternativeRouteName = nameof(BackOfficeGraphicsController) + "." + nameof(LoginLogoAlternative);
@@ -44,6 +45,11 @@ public class BackOfficeGraphicsController : Controller
[MapToApiVersion("1.0")]
public IActionResult Logo() => HandleFileRequest(_contentSettings.Value.BackOfficeLogo);
[HttpGet("logo-alternative", Name = LogoAlternativeRouteName)]
[AllowAnonymous]
[MapToApiVersion("1.0")]
public IActionResult LogoAlternative() => HandleFileRequest(_contentSettings.Value.BackOfficeLogoAlternative);
[HttpGet("login-logo", Name = LoginLogoRouteName)]
[AllowAnonymous]
[MapToApiVersion("1.0")]
@@ -1,4 +1,4 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Core.Security.OperationStatus;
@@ -20,7 +20,7 @@ public abstract class ClientCredentialsUserControllerBase : UserControllerBase
.Build()),
BackOfficeUserClientCredentialsOperationStatus.InvalidClientId => BadRequest(problemDetailsBuilder
.WithTitle("Invalid client ID")
.WithDetail("The specified client ID is invalid. A valid client ID can only contain [a-z], [A-Z], [0-9], and [-._~]. Furthermore, including the prefix it cannot be longer than 255 characters.")
.WithDetail("The specified client ID is invalid. A valid client ID can only contain [a-z], [A-Z], [0-9], and [-._~]. Furthermore, including the prefix it cannot be longer than 100 characters.")
.Build()),
_ => StatusCode(StatusCodes.Status500InternalServerError, problemDetailsBuilder
.WithTitle("Unknown client credentials operation status.")
File diff suppressed because it is too large Load Diff
@@ -1,4 +1,4 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.OpenApi.Models;
using Swashbuckle.AspNetCore.SwaggerGen;
@@ -59,7 +59,7 @@ public abstract class BackOfficeSecurityRequirementsOperationFilterBase : IOpera
{
operation.Responses.Add(StatusCodes.Status403Forbidden.ToString(), new OpenApiResponse()
{
Description = "The authenticated user do not have access to this resource"
Description = "The authenticated user does not have access to this resource"
});
}
}
@@ -0,0 +1,6 @@
namespace Umbraco.Cms.Api.Management.ViewModels.PublishedCache;
public class RebuildStatusModel
{
public bool IsRebuilding { get; set; }
}
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 468 468"><path d="M5.5 233.7C5.7 107.5 108.1 5.3 234.3 5.5s228.4 102.6 228.2 228.8c-.2 126.1-102.4 228.2-228.5 228.2C107.7 462.4 5.4 360 5.5 233.7zM229.4 312c-17.7.5-35.4-1.1-52.8-4.8-12.8-2.6-23.6-11.2-28.9-23.1-5.2-11.9-7.7-30.3-7.5-55.1.1-13 .9-25.9 2.4-38.8 1.5-12.6 2.9-22.9 4.4-31.1l1.6-8.1v-.7c0-2.3-1.6-4.2-3.8-4.6l-29.5-4.6h-.7c-2.2 0-4 1.5-4.5 3.6-.5 1.9-.8 3.3-1.7 7.8-1.7 8.7-3.2 17.1-5 29.5-1.8 13.4-2.9 26.9-3.3 40.4-.6 9.4-.6 18.9 0 28.3.7 25 5 45 12.9 59.9s21.2 25.7 40.1 32.3c18.8 6.6 45.1 9.8 78.7 9.6h4.2c33.7.1 59.9-3.1 78.7-9.6 18.8-6.6 32.2-17.3 40.1-32.3 7.9-15 12.2-34.9 12.9-59.9.6-9.4.6-18.9 0-28.3-.4-13.5-1.5-27-3.3-40.4-1.7-12.2-3.3-20.6-5-29.5-.9-4.5-1.2-5.9-1.7-7.8-.5-2.1-2.3-3.6-4.5-3.6h-.8l-29.5 4.6c-2.2.4-3.9 2.3-3.9 4.6v.7l1.6 8.1c1.5 8.2 3 18.5 4.5 31 1.5 12.9 2.3 25.9 2.4 38.8.2 24.8-2.3 43.2-7.6 55.1-5.2 11.9-15.9 20.5-28.6 23.2-17.3 3.7-35.1 5.3-52.8 4.8h-9.1z" fill="#283a97"/></svg>

After

Width:  |  Height:  |  Size: 980 B

@@ -1,51 +1 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 27.8.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_2_00000159465072208785227250000008040336475015305350_"
xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 1256.6 344.8"
style="enable-background:new 0 0 1256.6 344.8;" xml:space="preserve">
<style type="text/css">
.st0{fill:#283A97;}
</style>
<g id="Layer_1-2">
<g>
<path class="st0" d="M5,172.2C5.1,79.7,80.2,4.9,172.6,5s167.3,75.2,167.2,167.6c-0.1,92.4-75,167.2-167.4,167.2
C79.9,339.7,5,264.7,5,172.2L5,172.2L5,172.2z M169,229.5c-13,0.4-26-0.8-38.7-3.5c-9.4-1.9-17.3-8.2-21.2-17
c-3.8-8.7-5.6-22.2-5.5-40.4c0.1-9.5,0.7-19,1.8-28.4c1.1-9.2,2.2-16.8,3.2-22.8l1.1-5.9c0-0.2,0-0.3,0-0.5c0-1.7-1.2-3.1-2.8-3.4
l-21.6-3.4H85c-1.6,0-2.9,1.1-3.3,2.6c-0.4,1.4-0.6,2.4-1.2,5.7c-1.2,6.4-2.4,12.6-3.6,21.6c-1.3,9.8-2.1,19.7-2.4,29.6
c-0.5,6.9-0.5,13.8,0,20.7c0.5,18.3,3.7,33,9.4,43.9s15.6,18.8,29.4,23.6c13.8,4.8,33,7.2,57.7,7.1h3.1
c24.7,0.1,43.9-2.2,57.7-7.1c13.8-4.8,23.6-12.7,29.4-23.6c5.8-11,8.9-25.6,9.4-43.9c0.5-6.9,0.5-13.8,0-20.7
c-0.3-9.9-1.1-19.8-2.4-29.6c-1.3-9-2.4-15.1-3.6-21.6c-0.6-3.3-0.9-4.3-1.2-5.7c-0.4-1.5-1.7-2.6-3.3-2.6h-0.6l-21.6,3.4
c-1.6,0.3-2.8,1.7-2.8,3.4c0,0.2,0,0.3,0,0.5l1.2,5.9c1.1,6,2.2,13.6,3.3,22.7c1.1,9.4,1.7,18.9,1.8,28.4
c0.2,18.2-1.7,31.6-5.5,40.4c-3.8,8.7-11.6,15-20.9,17c-12.7,2.7-25.7,3.9-38.7,3.5L169,229.5L169,229.5z"/>
<g>
<path class="st0" d="M1155.2,186.1c0-33.4,9.6-56.9,48.2-56.9s48.2,23.5,48.2,56.9s-9.6,56.9-48.2,56.9
S1155.2,219.5,1155.2,186.1z M1227.1,186.1c0-23.2-3-36.5-23.6-36.5s-23.6,13.3-23.6,36.5s3.1,36.5,23.6,36.5
S1227.1,209.2,1227.1,186.1z"/>
<path class="st0" d="M487.2,238.7c0.6,1,1.6,1.6,2.8,1.6h9c1.8,0,3.2-1.4,3.2-3.2l0,0V135c0-1.8-1.4-3.2-3.2-3.2h-18
c-1.8,0-3.2,1.4-3.2,3.2v81c-7.7,4.3-16.4,6.5-25.2,6.3c-11.5,0-17.2-5-17.2-16.1V135c0-1.8-1.4-3.2-3.2-3.2h-17.9
c-1.8,0-3.2,1.4-3.2,3.2v73.4c0,20.8,9.8,34.5,37.3,34.5c12.6-0.1,24.9-4.1,35.2-11.3l3.2,7.2L487.2,238.7L487.2,238.7z"/>
<path class="st0" d="M689.8,163.7c0-20.6-10.2-34.5-35.9-34.5c-12.4,0-24.5,3.8-34.8,10.7c-4.6-6.7-13.3-10.7-27.8-10.7
c-11.8,0.2-23.3,4.2-32.8,11.3l-3.2-7.2l0,0c-0.6-1-1.6-1.7-2.8-1.7h-9c-1.8,0-3.2,1.4-3.2,3.2v102.1c0,1.8,1.4,3.2,3.2,3.2h18
c1.8,0,3.2-1.4,3.2-3.2v-80.8c6.9-4.1,14.8-6.3,22.8-6.3c9.8,0,15.4,3.6,15.4,14v73.4c0,1.8,1.4,3.2,3.2,3.2h18
c1.8,0,3.2-1.4,3.2-3.2v-81.1c6.8-4.2,14.8-6.4,22.8-6.4c9.6,0,15.4,3.6,15.4,14v73.4c0,1.8,1.4,3.2,3.2,3.2h18
c1.8,0,3.2-1.4,3.2-3.2l0,0L689.8,163.7L689.8,163.7z"/>
<path class="st0" d="M745.4,231.7c10.2,7.5,22.5,11.5,35.2,11.3c31.7,0,43.4-21.3,43.4-56.9s-11.7-56.9-43.4-56.9
c-10.3,0.1-20.4,3-29.1,8.5v-32.8c0-1.8-1.4-3.2-3.2-3.2h-18c-1.8,0-3.2,1.4-3.2,3.2v132.3c0,1.8,1.4,3.2,3.2,3.2h9
c1.2,0,2.2-0.6,2.8-1.6l0,0L745.4,231.7L745.4,231.7z M774.9,222.3c-8.2,0-16.3-2.1-23.4-6.3v-59.9c7.1-4.1,15.2-6.3,23.4-6.3
c21.3,0,24.5,16.3,24.5,36.2S796.2,222.3,774.9,222.3L774.9,222.3L774.9,222.3z"/>
<path class="st0" d="M913.9,150.5c-2.8-0.4-5.6-0.6-8.5-0.5c-9.7-0.4-19.3,1.9-27.8,6.5v80.6c0,1.8-1.4,3.2-3.2,3.2h-18
c-1.8,0-3.2-1.4-3.2-3.2V135c0-1.8,1.4-3.2,3.2-3.2h9c1.2,0,2.2,0.6,2.8,1.6l0,0l3.2,7.2c9.9-7.5,22-11.5,34.4-11.3
c2.8,0,5.7,0.2,8.5,0.7l0,0c1.7,0,3,2.7,3,4.4v13c0,1.8-1.4,3.2-3.2,3.2h-0.2"/>
<path class="st0" d="M976.1,190.2c-10.7,1.3-17.2,5.4-17.2,16.7c0,8.3,3.6,16.1,16.7,16.1c8.3,0.1,16.4-2.4,23.2-7.2v-28.1
L976.1,190.2L976.1,190.2z M1004.8,231.7C995.5,239,984,243,972.1,243c-28,0-37.3-17.4-37.3-34.8c0-23.5,15.2-33.4,39.7-35.4
l24.4-1.9v-5.4c0-11.1-5.2-15.4-21.3-15.4c-10.1,0-20.1,1.7-29.6,4.9c-0.3,0.1-0.7,0.1-1,0c-1.8,0-3.2-1.4-3.2-3.2v-14.4
c0-1.4,0.8-2.6,2.1-3.1l0,0c10.8-3.6,22.1-5.4,33.5-5.4c35.6,0,43.8,15.6,43.8,38.7v69.3c0,1.8-1.4,3.2-3.2,3.2h-9
c-1.2,0-2.2-0.6-2.8-1.6l0,0L1004.8,231.7L1004.8,231.7z"/>
<path class="st0" d="M1128.6,218c0.3,0,0.7,0,1,0c1.8,0,3.2,1.4,3.2,3.2v14.4c0,1.3-0.8,2.5-2,3l0,0c-8.9,3.2-18.4,4.8-27.9,4.6
c-38.5,0-50.3-23-50.3-56.9s11.8-56.9,50.3-56.9c9.4-0.2,18.9,1.2,27.8,4.4l0,0c1.2,0.5,2,1.7,2,3v14.5c0,1.8-1.4,3.2-3.2,3.2
c-0.4,0.1-0.7,0.1-1.1,0l0,0c-7.8-2.5-16-3.7-24.2-3.6c-21.1,0-27.2,14.4-27.2,35.4s6.1,35.4,27.2,35.4
c8.2,0.1,16.4-1.1,24.2-3.6"/>
</g>
</g>
</g>
</svg>
<svg version="1.1" id="prefix__Layer_2_00000159465072208785227250000008040336475015305350_" xmlns="http://www.w3.org/2000/svg" x="0" y="0" viewBox="0 0 1256.6 344.8" xml:space="preserve"><style>.prefix__st0{fill:#283a97}</style><g id="prefix__Layer_1-2"><path class="prefix__st0" d="M5 172.2C5.1 79.7 80.2 4.9 172.6 5s167.3 75.2 167.2 167.6c-.1 92.4-75 167.2-167.4 167.2C79.9 339.7 5 264.7 5 172.2zm164 57.3c-13 .4-26-.8-38.7-3.5-9.4-1.9-17.3-8.2-21.2-17-3.8-8.7-5.6-22.2-5.5-40.4.1-9.5.7-19 1.8-28.4 1.1-9.2 2.2-16.8 3.2-22.8l1.1-5.9v-.5c0-1.7-1.2-3.1-2.8-3.4l-21.6-3.4H85c-1.6 0-2.9 1.1-3.3 2.6-.4 1.4-.6 2.4-1.2 5.7-1.2 6.4-2.4 12.6-3.6 21.6-1.3 9.8-2.1 19.7-2.4 29.6-.5 6.9-.5 13.8 0 20.7.5 18.3 3.7 33 9.4 43.9s15.6 18.8 29.4 23.6c13.8 4.8 33 7.2 57.7 7.1h3.1c24.7.1 43.9-2.2 57.7-7.1 13.8-4.8 23.6-12.7 29.4-23.6 5.8-11 8.9-25.6 9.4-43.9.5-6.9.5-13.8 0-20.7-.3-9.9-1.1-19.8-2.4-29.6-1.3-9-2.4-15.1-3.6-21.6-.6-3.3-.9-4.3-1.2-5.7-.4-1.5-1.7-2.6-3.3-2.6h-.6l-21.6 3.4c-1.6.3-2.8 1.7-2.8 3.4v.5l1.2 5.9c1.1 6 2.2 13.6 3.3 22.7 1.1 9.4 1.7 18.9 1.8 28.4.2 18.2-1.7 31.6-5.5 40.4-3.8 8.7-11.6 15-20.9 17-12.7 2.7-25.7 3.9-38.7 3.5l-7.3.1zM1155.2 186.1c0-33.4 9.6-56.9 48.2-56.9s48.2 23.5 48.2 56.9-9.6 56.9-48.2 56.9-48.2-23.5-48.2-56.9zm71.9 0c0-23.2-3-36.5-23.6-36.5s-23.6 13.3-23.6 36.5 3.1 36.5 23.6 36.5 23.6-13.4 23.6-36.5zM487.2 238.7c.6 1 1.6 1.6 2.8 1.6h9c1.8 0 3.2-1.4 3.2-3.2V135c0-1.8-1.4-3.2-3.2-3.2h-18c-1.8 0-3.2 1.4-3.2 3.2v81c-7.7 4.3-16.4 6.5-25.2 6.3-11.5 0-17.2-5-17.2-16.1V135c0-1.8-1.4-3.2-3.2-3.2h-17.9c-1.8 0-3.2 1.4-3.2 3.2v73.4c0 20.8 9.8 34.5 37.3 34.5 12.6-.1 24.9-4.1 35.2-11.3l3.2 7.2.4-.1zM689.8 163.7c0-20.6-10.2-34.5-35.9-34.5-12.4 0-24.5 3.8-34.8 10.7-4.6-6.7-13.3-10.7-27.8-10.7-11.8.2-23.3 4.2-32.8 11.3l-3.2-7.2c-.6-1-1.6-1.7-2.8-1.7h-9c-1.8 0-3.2 1.4-3.2 3.2v102.1c0 1.8 1.4 3.2 3.2 3.2h18c1.8 0 3.2-1.4 3.2-3.2v-80.8c6.9-4.1 14.8-6.3 22.8-6.3 9.8 0 15.4 3.6 15.4 14v73.4c0 1.8 1.4 3.2 3.2 3.2h18c1.8 0 3.2-1.4 3.2-3.2v-81.1c6.8-4.2 14.8-6.4 22.8-6.4 9.6 0 15.4 3.6 15.4 14v73.4c0 1.8 1.4 3.2 3.2 3.2h18c1.8 0 3.2-1.4 3.2-3.2l-.1-73.4zM745.4 231.7c10.2 7.5 22.5 11.5 35.2 11.3 31.7 0 43.4-21.3 43.4-56.9s-11.7-56.9-43.4-56.9c-10.3.1-20.4 3-29.1 8.5v-32.8c0-1.8-1.4-3.2-3.2-3.2h-18c-1.8 0-3.2 1.4-3.2 3.2v132.3c0 1.8 1.4 3.2 3.2 3.2h9c1.2 0 2.2-.6 2.8-1.6l3.3-7.1zm29.5-9.4c-8.2 0-16.3-2.1-23.4-6.3v-59.9c7.1-4.1 15.2-6.3 23.4-6.3 21.3 0 24.5 16.3 24.5 36.2s-3.2 36.3-24.5 36.3zM913.9 150.5c-2.8-.4-5.6-.6-8.5-.5-9.7-.4-19.3 1.9-27.8 6.5v80.6c0 1.8-1.4 3.2-3.2 3.2h-18c-1.8 0-3.2-1.4-3.2-3.2V135c0-1.8 1.4-3.2 3.2-3.2h9c1.2 0 2.2.6 2.8 1.6l3.2 7.2c9.9-7.5 22-11.5 34.4-11.3 2.8 0 5.7.2 8.5.7 1.7 0 3 2.7 3 4.4v13c0 1.8-1.4 3.2-3.2 3.2h-.2M976.1 190.2c-10.7 1.3-17.2 5.4-17.2 16.7 0 8.3 3.6 16.1 16.7 16.1 8.3.1 16.4-2.4 23.2-7.2v-28.1l-22.7 2.5zm28.7 41.5C995.5 239 984 243 972.1 243c-28 0-37.3-17.4-37.3-34.8 0-23.5 15.2-33.4 39.7-35.4l24.4-1.9v-5.4c0-11.1-5.2-15.4-21.3-15.4-10.1 0-20.1 1.7-29.6 4.9-.3.1-.7.1-1 0-1.8 0-3.2-1.4-3.2-3.2v-14.4c0-1.4.8-2.6 2.1-3.1 10.8-3.6 22.1-5.4 33.5-5.4 35.6 0 43.8 15.6 43.8 38.7v69.3c0 1.8-1.4 3.2-3.2 3.2h-9c-1.2 0-2.2-.6-2.8-1.6l-3.4-6.8zM1128.6 218h1c1.8 0 3.2 1.4 3.2 3.2v14.4c0 1.3-.8 2.5-2 3-8.9 3.2-18.4 4.8-27.9 4.6-38.5 0-50.3-23-50.3-56.9s11.8-56.9 50.3-56.9c9.4-.2 18.9 1.2 27.8 4.4 1.2.5 2 1.7 2 3v14.5c0 1.8-1.4 3.2-3.2 3.2-.4.1-.7.1-1.1 0-7.8-2.5-16-3.7-24.2-3.6-21.1 0-27.2 14.4-27.2 35.4s6.1 35.4 27.2 35.4c8.2.1 16.4-1.1 24.2-3.6"/></g></svg>

Before

Width:  |  Height:  |  Size: 4.3 KiB

After

Width:  |  Height:  |  Size: 3.4 KiB

@@ -1,51 +1 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 27.8.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_2_00000109717856507224707600000004572449649498936508_"
xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 1256.6 344.8"
style="enable-background:new 0 0 1256.6 344.8;" xml:space="preserve">
<style type="text/css">
.st0{fill:#FFFFFF;}
</style>
<g id="Layer_1-2">
<g>
<path class="st0" d="M5,172.2C5.1,79.7,80.2,4.9,172.6,5s167.3,75.2,167.2,167.6c-0.1,92.4-75,167.2-167.4,167.2
C79.9,339.7,4.9,264.7,5,172.2L5,172.2L5,172.2z M169,229.5c-13,0.4-26-0.8-38.7-3.5c-9.4-1.9-17.3-8.2-21.2-17
c-3.8-8.7-5.6-22.2-5.5-40.4c0.1-9.5,0.7-19,1.8-28.4c1.1-9.2,2.2-16.8,3.2-22.8l1.1-5.9c0-0.2,0-0.3,0-0.5c0-1.7-1.2-3.1-2.8-3.4
l-21.6-3.4H85c-1.6,0-2.9,1.1-3.3,2.6c-0.4,1.4-0.6,2.4-1.2,5.7c-1.2,6.4-2.4,12.6-3.6,21.6c-1.3,9.8-2.1,19.7-2.4,29.6
c-0.5,6.9-0.5,13.8,0,20.7c0.5,18.3,3.7,33,9.4,43.9s15.6,18.8,29.4,23.6c13.8,4.8,33,7.2,57.7,7.1h3.1
c24.7,0.1,43.9-2.2,57.7-7.1c13.8-4.8,23.6-12.7,29.4-23.6c5.8-11,8.9-25.6,9.4-43.9c0.5-6.9,0.5-13.8,0-20.7
c-0.3-9.9-1.1-19.8-2.4-29.6c-1.3-8.9-2.4-15.1-3.6-21.6c-0.6-3.3-0.9-4.3-1.2-5.7c-0.4-1.5-1.7-2.6-3.3-2.6h-0.6l-21.6,3.4
c-1.6,0.3-2.8,1.7-2.8,3.4c0,0.2,0,0.3,0,0.5l1.2,5.9c1.1,6,2.2,13.6,3.3,22.7c1.1,9.4,1.7,18.9,1.8,28.4
c0.2,18.2-1.7,31.6-5.5,40.4c-3.8,8.7-11.6,15-20.9,17c-12.7,2.7-25.7,3.9-38.7,3.5L169,229.5L169,229.5z"/>
<g>
<path class="st0" d="M1155.2,186.1c0-33.4,9.6-56.9,48.2-56.9s48.2,23.5,48.2,56.9s-9.6,56.9-48.2,56.9
S1155.2,219.5,1155.2,186.1z M1227.1,186.1c0-23.2-3-36.5-23.6-36.5s-23.6,13.3-23.6,36.5s3.1,36.5,23.6,36.5
S1227.1,209.2,1227.1,186.1z"/>
<path class="st0" d="M487.2,238.7c0.6,1,1.6,1.6,2.8,1.6h9c1.8,0,3.2-1.4,3.2-3.2l0,0V135c0-1.8-1.4-3.2-3.2-3.2h-18
c-1.8,0-3.2,1.4-3.2,3.2v81c-7.7,4.3-16.4,6.5-25.2,6.3c-11.5,0-17.2-5-17.2-16.1V135c0-1.8-1.4-3.2-3.2-3.2h-17.9
c-1.8,0-3.2,1.4-3.2,3.2v73.4c0,20.8,9.8,34.5,37.3,34.5c12.6-0.1,24.9-4.1,35.2-11.3l3.2,7.2L487.2,238.7L487.2,238.7z"/>
<path class="st0" d="M689.8,163.7c0-20.6-10.2-34.5-35.9-34.5c-12.4,0-24.5,3.8-34.8,10.7c-4.6-6.7-13.3-10.7-27.8-10.7
c-11.8,0.2-23.3,4.2-32.8,11.3l-3.2-7.2l0,0c-0.6-1-1.6-1.7-2.8-1.7h-9c-1.8,0-3.2,1.4-3.2,3.2v102.1c0,1.8,1.4,3.2,3.2,3.2h18
c1.8,0,3.2-1.4,3.2-3.2v-80.8c6.9-4.1,14.8-6.3,22.8-6.3c9.8,0,15.4,3.6,15.4,14v73.4c0,1.8,1.4,3.2,3.2,3.2h18
c1.8,0,3.2-1.4,3.2-3.2v-81.1c6.8-4.2,14.8-6.4,22.8-6.4c9.6,0,15.4,3.6,15.4,14v73.4c0,1.8,1.4,3.2,3.2,3.2h18
c1.8,0,3.2-1.4,3.2-3.2l0,0L689.8,163.7L689.8,163.7z"/>
<path class="st0" d="M745.4,231.7c10.2,7.5,22.5,11.5,35.2,11.3c31.7,0,43.4-21.3,43.4-56.9s-11.7-56.9-43.4-56.9
c-10.3,0.1-20.4,3-29.1,8.5v-32.9c0-1.8-1.4-3.2-3.2-3.2h-18c-1.8,0-3.2,1.4-3.2,3.2v132.3c0,1.8,1.4,3.2,3.2,3.2h9
c1.2,0,2.2-0.6,2.8-1.6l0,0L745.4,231.7L745.4,231.7z M774.9,222.3c-8.2,0-16.3-2.1-23.4-6.3v-59.9c7.1-4.1,15.2-6.3,23.4-6.3
c21.3,0,24.5,16.3,24.5,36.2S796.2,222.3,774.9,222.3L774.9,222.3L774.9,222.3z"/>
<path class="st0" d="M913.9,150.5c-2.8-0.4-5.6-0.6-8.5-0.5c-9.7-0.4-19.3,1.9-27.8,6.5v80.6c0,1.8-1.4,3.2-3.2,3.2h-18
c-1.8,0-3.2-1.4-3.2-3.2V135c0-1.8,1.4-3.2,3.2-3.2h9c1.2,0,2.2,0.6,2.8,1.6l0,0l3.2,7.2c9.9-7.5,22-11.5,34.4-11.3
c2.8,0,5.7,0.2,8.5,0.7l0,0c1.7,0,3,2.7,3,4.4v13c0,1.8-1.4,3.2-3.2,3.2L913.9,150.5"/>
<path class="st0" d="M976.1,190.2c-10.7,1.3-17.2,5.4-17.2,16.7c0,8.3,3.6,16.1,16.7,16.1c8.3,0.1,16.4-2.4,23.2-7.2v-28.1
L976.1,190.2L976.1,190.2z M1004.8,231.7C995.5,239,984,243,972.1,243c-28,0-37.3-17.4-37.3-34.8c0-23.5,15.2-33.4,39.7-35.4
l24.4-1.9v-5.4c0-11.1-5.2-15.4-21.3-15.4c-10.1,0-20.1,1.7-29.6,4.9c-0.3,0.1-0.7,0.1-1,0c-1.8,0-3.2-1.4-3.2-3.2v-14.4
c0-1.4,0.8-2.6,2.1-3.1l0,0c10.8-3.6,22.1-5.4,33.5-5.4c35.6,0,43.8,15.6,43.8,38.7v69.3c0,1.8-1.4,3.2-3.2,3.2h-9
c-1.2,0-2.2-0.6-2.8-1.6l0,0L1004.8,231.7L1004.8,231.7z"/>
<path class="st0" d="M1128.6,218c0.3,0,0.7,0,1,0c1.8,0,3.2,1.4,3.2,3.2v14.4c0,1.3-0.8,2.5-2,3l0,0c-8.9,3.2-18.4,4.8-27.9,4.6
c-38.5,0-50.3-23-50.3-56.9s11.8-56.9,50.3-56.9c9.4-0.2,18.9,1.2,27.8,4.4l0,0c1.2,0.5,2,1.7,2,3v14.5c0,1.8-1.4,3.2-3.2,3.2
c-0.4,0.1-0.7,0.1-1.1,0l0,0c-7.8-2.5-16-3.7-24.2-3.6c-21.1,0-27.2,14.4-27.2,35.4s6.1,35.4,27.2,35.4
c8.2,0.1,16.4-1.1,24.2-3.6"/>
</g>
</g>
</g>
</svg>
<svg version="1.1" id="prefix__Layer_2_00000109717856507224707600000004572449649498936508_" xmlns="http://www.w3.org/2000/svg" x="0" y="0" viewBox="0 0 1256.6 344.8" xml:space="preserve"><style>.prefix__st0{fill:#fff}</style><g id="prefix__Layer_1-2"><path class="prefix__st0" d="M5 172.2C5.1 79.7 80.2 4.9 172.6 5s167.3 75.2 167.2 167.6c-.1 92.4-75 167.2-167.4 167.2C79.9 339.7 4.9 264.7 5 172.2zm164 57.3c-13 .4-26-.8-38.7-3.5-9.4-1.9-17.3-8.2-21.2-17-3.8-8.7-5.6-22.2-5.5-40.4.1-9.5.7-19 1.8-28.4 1.1-9.2 2.2-16.8 3.2-22.8l1.1-5.9v-.5c0-1.7-1.2-3.1-2.8-3.4l-21.6-3.4H85c-1.6 0-2.9 1.1-3.3 2.6-.4 1.4-.6 2.4-1.2 5.7-1.2 6.4-2.4 12.6-3.6 21.6-1.3 9.8-2.1 19.7-2.4 29.6-.5 6.9-.5 13.8 0 20.7.5 18.3 3.7 33 9.4 43.9s15.6 18.8 29.4 23.6c13.8 4.8 33 7.2 57.7 7.1h3.1c24.7.1 43.9-2.2 57.7-7.1 13.8-4.8 23.6-12.7 29.4-23.6 5.8-11 8.9-25.6 9.4-43.9.5-6.9.5-13.8 0-20.7-.3-9.9-1.1-19.8-2.4-29.6-1.3-8.9-2.4-15.1-3.6-21.6-.6-3.3-.9-4.3-1.2-5.7-.4-1.5-1.7-2.6-3.3-2.6h-.6l-21.6 3.4c-1.6.3-2.8 1.7-2.8 3.4v.5l1.2 5.9c1.1 6 2.2 13.6 3.3 22.7 1.1 9.4 1.7 18.9 1.8 28.4.2 18.2-1.7 31.6-5.5 40.4-3.8 8.7-11.6 15-20.9 17-12.7 2.7-25.7 3.9-38.7 3.5l-7.3.1zM1155.2 186.1c0-33.4 9.6-56.9 48.2-56.9s48.2 23.5 48.2 56.9-9.6 56.9-48.2 56.9-48.2-23.5-48.2-56.9zm71.9 0c0-23.2-3-36.5-23.6-36.5s-23.6 13.3-23.6 36.5 3.1 36.5 23.6 36.5 23.6-13.4 23.6-36.5zM487.2 238.7c.6 1 1.6 1.6 2.8 1.6h9c1.8 0 3.2-1.4 3.2-3.2V135c0-1.8-1.4-3.2-3.2-3.2h-18c-1.8 0-3.2 1.4-3.2 3.2v81c-7.7 4.3-16.4 6.5-25.2 6.3-11.5 0-17.2-5-17.2-16.1V135c0-1.8-1.4-3.2-3.2-3.2h-17.9c-1.8 0-3.2 1.4-3.2 3.2v73.4c0 20.8 9.8 34.5 37.3 34.5 12.6-.1 24.9-4.1 35.2-11.3l3.2 7.2.4-.1zM689.8 163.7c0-20.6-10.2-34.5-35.9-34.5-12.4 0-24.5 3.8-34.8 10.7-4.6-6.7-13.3-10.7-27.8-10.7-11.8.2-23.3 4.2-32.8 11.3l-3.2-7.2c-.6-1-1.6-1.7-2.8-1.7h-9c-1.8 0-3.2 1.4-3.2 3.2v102.1c0 1.8 1.4 3.2 3.2 3.2h18c1.8 0 3.2-1.4 3.2-3.2v-80.8c6.9-4.1 14.8-6.3 22.8-6.3 9.8 0 15.4 3.6 15.4 14v73.4c0 1.8 1.4 3.2 3.2 3.2h18c1.8 0 3.2-1.4 3.2-3.2v-81.1c6.8-4.2 14.8-6.4 22.8-6.4 9.6 0 15.4 3.6 15.4 14v73.4c0 1.8 1.4 3.2 3.2 3.2h18c1.8 0 3.2-1.4 3.2-3.2l-.1-73.4zM745.4 231.7c10.2 7.5 22.5 11.5 35.2 11.3 31.7 0 43.4-21.3 43.4-56.9s-11.7-56.9-43.4-56.9c-10.3.1-20.4 3-29.1 8.5v-32.9c0-1.8-1.4-3.2-3.2-3.2h-18c-1.8 0-3.2 1.4-3.2 3.2v132.3c0 1.8 1.4 3.2 3.2 3.2h9c1.2 0 2.2-.6 2.8-1.6l3.3-7zm29.5-9.4c-8.2 0-16.3-2.1-23.4-6.3v-59.9c7.1-4.1 15.2-6.3 23.4-6.3 21.3 0 24.5 16.3 24.5 36.2s-3.2 36.3-24.5 36.3zM913.9 150.5c-2.8-.4-5.6-.6-8.5-.5-9.7-.4-19.3 1.9-27.8 6.5v80.6c0 1.8-1.4 3.2-3.2 3.2h-18c-1.8 0-3.2-1.4-3.2-3.2V135c0-1.8 1.4-3.2 3.2-3.2h9c1.2 0 2.2.6 2.8 1.6l3.2 7.2c9.9-7.5 22-11.5 34.4-11.3 2.8 0 5.7.2 8.5.7 1.7 0 3 2.7 3 4.4v13c0 1.8-1.4 3.2-3.2 3.2l-.2-.1M976.1 190.2c-10.7 1.3-17.2 5.4-17.2 16.7 0 8.3 3.6 16.1 16.7 16.1 8.3.1 16.4-2.4 23.2-7.2v-28.1l-22.7 2.5zm28.7 41.5C995.5 239 984 243 972.1 243c-28 0-37.3-17.4-37.3-34.8 0-23.5 15.2-33.4 39.7-35.4l24.4-1.9v-5.4c0-11.1-5.2-15.4-21.3-15.4-10.1 0-20.1 1.7-29.6 4.9-.3.1-.7.1-1 0-1.8 0-3.2-1.4-3.2-3.2v-14.4c0-1.4.8-2.6 2.1-3.1 10.8-3.6 22.1-5.4 33.5-5.4 35.6 0 43.8 15.6 43.8 38.7v69.3c0 1.8-1.4 3.2-3.2 3.2h-9c-1.2 0-2.2-.6-2.8-1.6l-3.4-6.8zM1128.6 218h1c1.8 0 3.2 1.4 3.2 3.2v14.4c0 1.3-.8 2.5-2 3-8.9 3.2-18.4 4.8-27.9 4.6-38.5 0-50.3-23-50.3-56.9s11.8-56.9 50.3-56.9c9.4-.2 18.9 1.2 27.8 4.4 1.2.5 2 1.7 2 3v14.5c0 1.8-1.4 3.2-3.2 3.2-.4.1-.7.1-1.1 0-7.8-2.5-16-3.7-24.2-3.6-21.1 0-27.2 14.4-27.2 35.4s6.1 35.4 27.2 35.4c8.2.1 16.4-1.1 24.2-3.6"/></g></svg>

Before

Width:  |  Height:  |  Size: 4.3 KiB

After

Width:  |  Height:  |  Size: 3.4 KiB

@@ -23,6 +23,7 @@ public class ContentSettings
internal const string StaticLoginLogoImage = "assets/logo_light.svg";
internal const string StaticLoginLogoImageAlternative = "assets/logo_dark.svg";
internal const string StaticBackOfficeLogo = "assets/logo.svg";
internal const string StaticBackOfficeLogoAlternative = "assets/logo_blue.svg";
internal const bool StaticHideBackOfficeLogo = false;
internal const bool StaticDisableDeleteWhenReferenced = false;
internal const bool StaticDisableUnpublishWhenReferenced = false;
@@ -88,9 +89,18 @@ public class ContentSettings
/// <summary>
/// Gets or sets a value for the path to the backoffice logo.
/// </summary>
/// <remarks>The alternative version of this logo can be found at <see cref="BackOfficeLogoAlternative"/>.</remarks>
[DefaultValue(StaticBackOfficeLogo)]
public string BackOfficeLogo { get; set; } = StaticBackOfficeLogo;
/// <summary>
/// Gets or sets a value for the path to the alternative backoffice logo, which can be shown
/// on top of a light background.
/// </summary>
/// <remarks>This is the alternative version to the regular logo found at <see cref="BackOfficeLogo"/>.</remarks>
[DefaultValue(StaticBackOfficeLogoAlternative)]
public string BackOfficeLogoAlternative { get; set; } = StaticBackOfficeLogoAlternative;
/// <summary>
/// Gets or sets a value indicating whether to hide the backoffice umbraco logo or not.
/// </summary>
+12 -2
View File
@@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<language alias="da" intName="Danish" localName="dansk" lcid="6" culture="da-DK">
<creator>
<name>The Umbraco community</name>
@@ -114,13 +114,23 @@ Mange hilsner fra Umbraco robotten
<key alias="entriesExceed"><![CDATA[Maksimum %0% element(er), <strong>%1%</strong> for mange.]]></key>
<key alias="entriesAreasMismatch">Ét eller flere områder lever ikke op til kravene for antal indholdselementer.</key>
<key alias="invalidMediaType">Den valgte medie type er ugyldig.</key>
<key alias="invalidContentType">Det valgte indhold er af en ugyldig type.</key>
<key alias="missingContent">Det valgte indhold eksistere ikke.</key>
<key alias="multipleMediaNotAllowed">Det er kun tilladt at vælge ét medie.</key>
<key alias="invalidStartNode">Valgt medie kommer fra en ugyldig mappe.</key>
<key alias="invalidStartNode">Valgt indhold kommer fra en ugyldig mappe.</key>
<key alias="outOfRangeMinimum">Værdien %0% er mindre end det tilladte minimum af %1%.</key>
<key alias="outOfRangeMaximum">Værdien %0% er større end det tilladte maksimum af %1%.</key>
<key alias="outOfRangeSingleItemMinimum">Den ene enhed givet er mindre end det tilladte minimum af %1%.</key>
<key alias="outOfRangeMultipleItemsMinimum">De %0% enheder givet er mindre end det tilladte minimum af %1%.</key>
<key alias="outOfRangeMultipleItemsMaximum">De %0% enheder givet er større end det tilladte minimum af %1%.</key>
<key alias="invalidStep">Værdien %0% passer ikke med den konfigureret trin værdi af %1% og mindste værdi af %2%.</key>
<key alias="unexpectedRange">Værdien %0% forventes ikke at indeholde et spænd.</key>
<key alias="stringLengthExceeded">Tekststrengen er længere end den tilladte længde på %0% tegn, %1% for mange.</key>
<key alias="invalidRange">Værdien %0% forventes at have en værdi der er større end fra værdien.</key>
<key alias="invalidObjectType">Det valgte indhold er af den forkerte type.</key>
<key alias="notOneOfOptions">"Værdien '%0%' er ikke en af de tilgængelige valgmuligheder.</key>
<key alias="multipleNotOneOfOptions">Værdierne '%0%' er ikke tilstede i de tilgængelige valgmuligheder.</key>
<key alias="invalidColor">"Den valgte farve '%0%' er ikke en af de tilgængelige valgmuligheder.</key>
</area>
<area alias="recycleBin">
<key alias="contentTrashed">Slettet indhold med Id: {0} Relateret til original "parent" med id: {1}</key>
+12 -1
View File
@@ -378,6 +378,7 @@
<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="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>
<key alias="invalidUserGroupName">Invalid user group name</key>
@@ -389,12 +390,22 @@
<key alias="duplicateUsername">Username '%0%' is already taken</key>
<key alias="outOfRangeMinimum">The value %0% is less than the allowed minimum value of %1%</key>
<key alias="outOfRangeMaximum">The value %0% is greater than the allowed maximum value of %1%</key>
<key alias="outOfRangeSingleItemMinimum">The 1 item provided is less than the allowed minimum of %1%</key>
<key alias="outOfRangeMultipleItemsMinimum">The %0% items provided are less than the allowed minimum of %1%</key>
<key alias="outOfRangeMultipleItemsMaximum">The %0% items provided are greater than the allowed maximum of %1%</key>
<key alias="invalidStep">The value %0% does not correspond with the configured step value of %1% and minimum value of %2%</key>
<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="invalidMediaType">The chosen media type is invalid.</key>
<key alias="invalidContentType">The chosen content is of invalid type.</key>
<key alias="missingContent">The chosen content does not exist.</key>
<key alias="multipleMediaNotAllowed">Multiple selected media is not allowed.</key>
<key alias="invalidStartNode">The selected media is from the wrong folder.</key>
<key alias="notOneOfOptions">The value '%0%' is not one of the available options.</key>
<key alias="multipleNotOneOfOptions">The values '%0%' are not found in the the available options.</key>
<key alias="invalidColor">"The selected colour '%0%' is not one of the available options.</key>
<key alias="invalidStartNode">The selected item is from the wrong folder.</key>
<key alias="invalidObjectType">The selected item is of the wrong type.</key>
<key alias="notOneOfOptions">"The value '%0%' is not one of the available options.</key>
</area>
<area alias="healthcheck">
<!-- The following keys get these tokens passed in:
@@ -387,15 +387,25 @@
<key alias="duplicateUsername">Username '%0%' is already taken</key>
<key alias="outOfRangeMinimum">The value %0% is less than the allowed minimum value of %1%</key>
<key alias="outOfRangeMaximum">The value %0% is greater than the allowed maximum value of %1%</key>
<key alias="outOfRangeSingleItemMinimum">The 1 item provided is less than the allowed minimum of %1%</key>
<key alias="outOfRangeMultipleItemsMinimum">The %0% items provided are less than the allowed minimum of %1%</key>
<key alias="outOfRangeMultipleItemsMaximum">The %0% items provided are greater than the allowed maximum of %1%</key>
<key alias="invalidStep">The value %0% does not correspond with the configured step value of %1% and minimum value of %2%</key>
<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="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>
<key alias="invalidContentType">The chosen content is of invalid type.</key>
<key alias="missingContent">The chosen content does not exist.</key>
<key alias="multipleMediaNotAllowed">Multiple selected media is not allowed.</key>
<key alias="invalidStartNode">The selected media is from the wrong folder.</key>
<key alias="invalidStartNode">The selected item is from the wrong folder.</key>
<key alias="invalidObjectType">The selected item is of the wrong type.</key>
<key alias="notOneOfOptions">"The value '%0%' is not one of the available options.</key>
<key alias="multipleNotOneOfOptions">The values '%0%' are not found in the the available options.</key>
<key alias="invalidColor">The selected color '%0%' is not one of the available options.</key>
</area>
<area alias="healthcheck">
<!-- The following keys get these tokens passed in:
@@ -12,6 +12,9 @@ public class PackageManifest
public bool AllowTelemetry { get; set; } = true;
[Obsolete("Use AllowTelemetry instead. This property will be removed in future versions.")]
public bool AllowPackageTelemetry { get; set; } = true;
public required object[] Extensions { get; set; }
public PackageManifestImportmap? Importmap { get; set; }
@@ -11,7 +11,7 @@ public static class ContentBaseExtensions
private static DefaultUrlSegmentProvider? _defaultUrlSegmentProvider;
/// <summary>
/// Gets the URL segment for a specified content and culture.
/// Gets a single URL segment for a specified content and culture.
/// </summary>
/// <param name="content">The content.</param>
/// <param name="shortStringHelper"></param>
@@ -19,29 +19,73 @@ public static class ContentBaseExtensions
/// <param name="culture">The culture.</param>
/// <param name="published">Whether to get the published or draft.</param>
/// <returns>The URL segment.</returns>
/// <remarks>
/// If more than one URL segment provider is available, the first one that returns a non-null value will be returned.
/// </remarks>
public static string? GetUrlSegment(this IContentBase content, IShortStringHelper shortStringHelper, IEnumerable<IUrlSegmentProvider> urlSegmentProviders, string? culture = null, bool published = true)
{
if (content == null)
{
throw new ArgumentNullException(nameof(content));
}
var urlSegment = GetUrlSegments(content, urlSegmentProviders, culture, published).FirstOrDefault();
if (urlSegmentProviders == null)
{
throw new ArgumentNullException(nameof(urlSegmentProviders));
}
// Ensure we have at least the segment from the default URL provider returned.
urlSegment ??= GetDefaultUrlSegment(shortStringHelper, content, culture, published);
var url = urlSegmentProviders.Select(p => p.GetUrlSegment(content, published, culture)).FirstOrDefault(u => u != null);
if (url == null)
return urlSegment;
}
/// <summary>
/// Gets all URL segments for a specified content and culture.
/// </summary>
/// <param name="content">The content.</param>
/// <param name="shortStringHelper"></param>
/// <param name="urlSegmentProviders"></param>
/// <param name="culture">The culture.</param>
/// <param name="published">Whether to get the published or draft.</param>
/// <returns>The collection of URL segments.</returns>
public static IEnumerable<string> GetUrlSegments(this IContentBase content, IShortStringHelper shortStringHelper, IEnumerable<IUrlSegmentProvider> urlSegmentProviders, string? culture = null, bool published = true)
{
var urlSegments = GetUrlSegments(content, urlSegmentProviders, culture, published).Distinct().ToList();
// Ensure we have at least the segment from the default URL provider returned.
if (urlSegments.Count == 0)
{
if (_defaultUrlSegmentProvider == null)
var defaultUrlSegment = GetDefaultUrlSegment(shortStringHelper, content, culture, published);
if (defaultUrlSegment is not null)
{
_defaultUrlSegmentProvider = new DefaultUrlSegmentProvider(shortStringHelper);
urlSegments.Add(defaultUrlSegment);
}
url = _defaultUrlSegmentProvider.GetUrlSegment(content, culture); // be safe
}
return url;
return urlSegments;
}
private static IEnumerable<string> GetUrlSegments(
IContentBase content,
IEnumerable<IUrlSegmentProvider> urlSegmentProviders,
string? culture,
bool published)
{
foreach (IUrlSegmentProvider urlSegmentProvider in urlSegmentProviders)
{
var segment = urlSegmentProvider.GetUrlSegment(content, published, culture);
if (string.IsNullOrEmpty(segment) == false)
{
yield return segment;
if (urlSegmentProvider.AllowAdditionalSegments is false)
{
yield break;
}
}
}
}
private static string? GetDefaultUrlSegment(
IShortStringHelper shortStringHelper,
IContentBase content,
string? culture,
bool published)
{
_defaultUrlSegmentProvider ??= new DefaultUrlSegmentProvider(shortStringHelper);
return _defaultUrlSegmentProvider.GetUrlSegment(content, published, culture);
}
}
@@ -10,4 +10,6 @@ public class ContentPropertySettings
public ISet<string> ReservedFieldNames => _reservedFieldNames;
public bool AddReservedFieldName(string name) => _reservedFieldNames.Add(name);
public void AddReservedFieldNames(ISet<string> names) => _reservedFieldNames.UnionWith(names);
}
@@ -10,4 +10,6 @@ public class MediaPropertySettings
public ISet<string> ReservedFieldNames => _reservedFieldNames;
public bool AddReservedFieldName(string name) => _reservedFieldNames.Add(name);
public void AddReservedFieldNames(ISet<string> names) => _reservedFieldNames.UnionWith(names);
}
@@ -10,4 +10,6 @@ public class MemberPropertySettings
public ISet<string> ReservedFieldNames => _reservedFieldNames;
public bool AddReservedFieldName(string name) => _reservedFieldNames.Add(name);
public void AddReservedFieldNames(ISet<string> names) => _reservedFieldNames.UnionWith(names);
}
@@ -1,4 +1,4 @@
namespace Umbraco.Cms.Core.Models.PublishedContent;
namespace Umbraco.Cms.Core.Models.PublishedContent;
public interface IPublishedMember : IPublishedContent
{
@@ -1,9 +1,32 @@
namespace Umbraco.Cms.Core.Models;
/// <summary>
/// Represents a URL segment for a published document.
/// </summary>
public class PublishedDocumentUrlSegment
{
/// <summary>
/// Gets or sets the document key.
/// </summary>
public required Guid DocumentKey { get; set; }
/// <summary>
/// Gets or sets the language Id.
/// </summary>
public required int LanguageId { get; set; }
/// <summary>
/// Gets or sets the URL segment string.
/// </summary>
public required string UrlSegment { get; set; }
/// <summary>
/// Gets or sets a value indicating whether the URL segment is for a draft.
/// </summary>
public required bool IsDraft { get; set; }
/// <summary>
/// Gets or sets a value indicating whether the URL segment is the primary one (first resolved from the collection of URL providers).
/// </summary>
public required bool IsPrimary { get; set; }
}
@@ -17,6 +17,9 @@ public class MultiNodePickerConfiguration : IIgnoreUserStartNodesConfig
[ConfigurationField("maxNumber")]
public int MaxNumber { get; set; }
[ConfigurationField("filter")]
public string? Filter { get; set; }
[ConfigurationField(Constants.DataTypes.ReservedPreValueKeys.IgnoreUserStartNodes)]
public bool IgnoreUserStartNodes { get; set; }
}
@@ -1,4 +1,4 @@
using Umbraco.Cms.Core.IO;
using Umbraco.Cms.Core.IO;
using Umbraco.Cms.Core.PropertyEditors.Validators;
using Umbraco.Cms.Core.Serialization;
using Umbraco.Cms.Core.Services;
@@ -6,6 +6,7 @@ using Umbraco.Cms.Core.Strings;
namespace Umbraco.Cms.Core.PropertyEditors;
[Obsolete("This is no longer used and has been migrated to an internal class within RadioButtonsPropertyEditor. Scheduled for removal in Umbraco 17.")]
public class RadioValueEditor : DataValueEditor
{
public RadioValueEditor(
@@ -1,8 +1,13 @@
using System.ComponentModel.DataAnnotations;
using Microsoft.Extensions.DependencyInjection;
using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Core.IO;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.Validation;
using Umbraco.Cms.Core.Serialization;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Strings;
using Umbraco.Extensions;
namespace Umbraco.Cms.Core.PropertyEditors;
@@ -12,23 +17,27 @@ namespace Umbraco.Cms.Core.PropertyEditors;
/// </summary>
public class TextOnlyValueEditor : DataValueEditor
{
[Obsolete($"Use the constructor that does not accept {nameof(ILocalizedTextService)}. Will be removed in V15.")]
public TextOnlyValueEditor(
DataEditorAttribute attribute,
ILocalizedTextService localizedTextService,
IShortStringHelper shortStringHelper,
IJsonSerializer jsonSerializer,
IIOHelper ioHelper)
: this(attribute, shortStringHelper, jsonSerializer, ioHelper)
{
}
: base(shortStringHelper, jsonSerializer, ioHelper, attribute) =>
Validators.Add(new LengthValidator(localizedTextService));
[Obsolete($"Use the constructor that accepts {nameof(ILocalizedTextService)}. Will be removed in V16.")]
public TextOnlyValueEditor(
DataEditorAttribute attribute,
IShortStringHelper shortStringHelper,
IJsonSerializer jsonSerializer,
IIOHelper ioHelper)
: base(shortStringHelper, jsonSerializer, ioHelper, attribute)
: this(
attribute,
StaticServiceProvider.Instance.GetRequiredService<ILocalizedTextService>(),
shortStringHelper,
jsonSerializer,
ioHelper)
{
}
@@ -65,4 +74,48 @@ public class TextOnlyValueEditor : DataValueEditor
" can only be used with string based property editors");
}
}
/// <summary>
/// A common length validator for both textbox and text area.
/// </summary>
internal class LengthValidator : IValueValidator
{
private readonly ILocalizedTextService _localizedTextService;
public LengthValidator(ILocalizedTextService localizedTextService)
{
_localizedTextService = localizedTextService;
}
public IEnumerable<ValidationResult> Validate(object? value, string? valueType, object? dataTypeConfiguration,
PropertyValidationContext validationContext)
{
int? maxCharacters = dataTypeConfiguration switch
{
TextAreaConfiguration areaConfiguration => areaConfiguration.MaxChars,
TextboxConfiguration textboxConfiguration => textboxConfiguration.MaxChars,
_ => null,
};
if (maxCharacters is null)
{
return [];
}
if (value is string typedValue && typedValue.Length > maxCharacters)
{
return
[
new ValidationResult(
_localizedTextService.Localize(
"validation",
"stringLengthExceeded",
[maxCharacters.ToString(), (typedValue.Length - maxCharacters).ToString() ]),
["value'"])
];
}
return [];
}
}
}
@@ -20,13 +20,22 @@ public class TypedJsonValidatorRunner<TValue, TConfiguration> : IValueValidator
private readonly IJsonSerializer _jsonSerializer;
private readonly ITypedJsonValidator<TValue, TConfiguration>[] _validators;
/// <summary>
/// Initializes a new instance of the <see cref="TypedJsonValidatorRunner{TValue, TConfiguration}"/> class.
/// </summary>
/// <param name="jsonSerializer">The JSON serializer.</param>
/// <param name="validators">The collection of validators to run.</param>
public TypedJsonValidatorRunner(IJsonSerializer jsonSerializer, params ITypedJsonValidator<TValue, TConfiguration>[] validators)
{
_jsonSerializer = jsonSerializer;
_validators = validators;
}
public IEnumerable<ValidationResult> Validate(object? value, string? valueType, object? dataTypeConfiguration,
/// <inheritdoc/>
public IEnumerable<ValidationResult> Validate(
object? value,
string? valueType,
object? dataTypeConfiguration,
PropertyValidationContext validationContext)
{
var validationResults = new List<ValidationResult>();
@@ -36,7 +45,8 @@ public class TypedJsonValidatorRunner<TValue, TConfiguration> : IValueValidator
return validationResults;
}
if (value is null || _jsonSerializer.TryDeserialize(value, out TValue? deserializedValue) is false)
TValue? deserializedValue = null;
if (value is not null && _jsonSerializer.TryDeserialize(value, out deserializedValue) is false)
{
return validationResults;
}
@@ -1,3 +1,5 @@
using Umbraco.Cms.Core.Services.Navigation;
namespace Umbraco.Cms.Core.PropertyEditors.Validation;
/// <summary>
@@ -26,4 +28,49 @@ public static class ValidationHelper
return (value - min) % step == 0;
}
/// <summary>
/// Checks if all provided entities has the start node as an ancestor.
/// </summary>
/// <param name="entityKeys">Keys to check.</param>
/// <param name="startNode">The configured start node.</param>
/// <param name="navigationQueryService">The navigation query service to use for the checks.</param>
/// <returns>True if the startnode key is in the ancestry tree.</returns>
public static bool HasValidStartNode(IEnumerable<Guid> entityKeys, Guid startNode, INavigationQueryService navigationQueryService)
{
List<Guid> uniqueParentKeys = [];
foreach (Guid distinctMediaKey in entityKeys.Distinct())
{
if (navigationQueryService.TryGetParentKey(distinctMediaKey, out Guid? parentKey) is false)
{
continue;
}
// If there is a start node, the media must have a parent.
if (parentKey is null)
{
return false;
}
uniqueParentKeys.Add(parentKey.Value);
}
IEnumerable<Guid> parentKeysNotInStartNode = uniqueParentKeys.Where(x => x != startNode);
foreach (Guid parentKey in parentKeysNotInStartNode)
{
if (navigationQueryService.TryGetAncestorsKeys(parentKey, out IEnumerable<Guid> foundAncestorKeys) is false)
{
// We couldn't find the parent node, so we fail.
return false;
}
Guid[] ancestorKeys = foundAncestorKeys.ToArray();
if (ancestorKeys.Length == 0 || ancestorKeys.Contains(startNode) is false)
{
return false;
}
}
return true;
}
}
@@ -1,24 +1,45 @@
using System.ComponentModel.DataAnnotations;
using Microsoft.Extensions.DependencyInjection;
using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Core.Models.Validation;
using Umbraco.Cms.Core.Services;
using Umbraco.Extensions;
namespace Umbraco.Cms.Core.PropertyEditors.Validators;
/// <summary>
/// A validator that validates an email address
/// A validator that validates an email address.
/// </summary>
public sealed class EmailValidator : IValueValidator
{
private readonly ILocalizedTextService _localizedTextService;
/// <summary>
/// Initializes a new instance of the <see cref="EmailValidator"/> class.
/// </summary>
[Obsolete("Please use the constructor taking all parameters. Scheduled for removal in Umbraco 17.")]
public EmailValidator()
: this(StaticServiceProvider.Instance.GetRequiredService<ILocalizedTextService>())
{
}
/// <summary>
/// Initializes a new instance of the <see cref="EmailValidator"/> class.
/// </summary>
public EmailValidator(ILocalizedTextService localizedTextService) => _localizedTextService = localizedTextService;
/// <inheritdoc />
public IEnumerable<ValidationResult> Validate(object? value, string? valueType, object? dataTypeConfiguration, PropertyValidationContext validationContext)
{
var asString = value == null ? string.Empty : value.ToString();
var valueAsString = value?.ToString() ?? string.Empty;
var emailVal = new EmailAddressAttribute();
var emailAddressAttribute = new EmailAddressAttribute();
if (asString != string.Empty && emailVal.IsValid(asString) == false)
if (valueAsString != string.Empty && emailAddressAttribute.IsValid(valueAsString) == false)
{
// TODO: localize these!
yield return new ValidationResult("Email is invalid", new[] { "value" });
yield return new ValidationResult(
_localizedTextService.Localize("validation", "invalidEmail", [valueAsString]),
["value"]);
}
}
}
@@ -1,11 +1,34 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations;
using Microsoft.Extensions.DependencyInjection;
using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Core.Models.Validation;
using Umbraco.Cms.Core.Services;
using Umbraco.Extensions;
namespace Umbraco.Cms.Core.PropertyEditors.Validators;
/// <summary>
/// A value validator for property editors that handle multiple values from a configured list of options.
/// </summary>
public class MultipleValueValidator : IValueValidator
{
private readonly ILocalizedTextService _localizedTextService;
/// <summary>
/// Initializes a new instance of the <see cref="MultipleValueValidator"/> class.
/// </summary>
[Obsolete("Please use the constructor that takes all parameters. Scheduled for removal in Umbraco 17.")]
public MultipleValueValidator()
: this(StaticServiceProvider.Instance.GetRequiredService<ILocalizedTextService>())
{
}
/// <summary>
/// Initializes a new instance of the <see cref="MultipleValueValidator"/> class.
/// </summary>
public MultipleValueValidator(ILocalizedTextService localizedTextService) => _localizedTextService = localizedTextService;
/// <inheritdoc/>
public IEnumerable<ValidationResult> Validate(object? value, string? valueType, object? dataTypeConfiguration, PropertyValidationContext validationContext)
{
// don't validate if empty
@@ -24,13 +47,21 @@ public class MultipleValueValidator : IValueValidator
yield break;
}
foreach (var selectedValue in values)
var invalidValues = values
.Where(x => valueListConfiguration.Items.Contains(x) is false)
.ToList();
if (invalidValues.Count == 1)
{
if (valueListConfiguration.Items.Contains(selectedValue) is false)
{
yield return new ValidationResult(
$"The value {selectedValue} is not a part of the pre-values", ["items"]);
}
yield return new ValidationResult(
_localizedTextService.Localize("validation", "notOneOfOptions", [invalidValues[0]]),
["value"]);
}
else if (invalidValues.Count > 1)
{
yield return new ValidationResult(
_localizedTextService.Localize("validation", "multipleNotOneOfOptions", [string.Join(", ", invalidValues)]),
["value"]);
}
}
}
@@ -1,9 +1,10 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations;
using Umbraco.Cms.Core.Models.Validation;
using Umbraco.Extensions;
namespace Umbraco.Cms.Core.PropertyEditors.Validators;
[Obsolete("This is no longer used and has been migrated to an internal class within RadioButtonsPropertyEditor. Scheduled for removal in Umbraco 17.")]
public class RadioValueValidator : IValueValidator
{
public IEnumerable<ValidationResult> Validate(object? value, string? valueType, object? dataTypeConfiguration,
@@ -1,8 +1,33 @@
namespace Umbraco.Cms.Core.PublishedCache;
namespace Umbraco.Cms.Core.PublishedCache;
/// <summary>
/// Defines operations for rebuild of the published content cache in the database.
/// </summary>
public interface IDatabaseCacheRebuilder
{
/// <summary>
/// Indicates if the database cache is in the process of being rebuilt.
/// </summary>
/// <returns></returns>
bool IsRebuilding() => false;
/// <summary>
/// Rebuilds the database cache.
/// </summary>
[Obsolete("Use the overload with the useBackgroundThread parameter. Scheduled for removal in Umbraco 17.")]
void Rebuild();
/// <summary>
/// Rebuilds the database cache, optionally using a background thread.
/// </summary>
/// <param name="useBackgroundThread">Flag indicating whether to use a background thread for the operation and immediately return to the caller.</param>
void Rebuild(bool useBackgroundThread)
#pragma warning disable CS0618 // Type or member is obsolete
=> Rebuild();
#pragma warning restore CS0618 // Type or member is obsolete
/// <summary>
/// Rebuids the database cache if the configured serializer has changed.
/// </summary>
void RebuildDatabaseCacheIfSerializerChanged();
}
+2 -7
View File
@@ -1267,7 +1267,7 @@ public class ContentService : RepositoryService, IContentService
// we need to guard against unsaved changes before proceeding; the content will be saved, but we're not firing any saved notifications
if (HasUnsavedChanges(content))
{
return new PublishResult(PublishResultType.FailedPublishUnsavedChanges, EventMessagesFactory.Get(), content);
return new PublishResult(PublishResultType.FailedPublishUnsavedChanges, evtMsgs, content);
}
if (content.Name != null && content.Name.Length > 255)
@@ -1324,13 +1324,8 @@ public class ContentService : RepositoryService, IContentService
// Change state to publishing
content.PublishedState = PublishedState.Publishing;
var publishingNotification = new ContentPublishingNotification(content, evtMsgs);
if (scope.Notifications.PublishCancelable(publishingNotification))
{
return new PublishResult(PublishResultType.FailedPublishCancelledByEvent, evtMsgs, content);
}
PublishResult result = CommitDocumentChangesInternal(scope, content, evtMsgs, allLangs, publishingNotification.State, userId);
PublishResult result = CommitDocumentChangesInternal(scope, content, evtMsgs, allLangs, new Dictionary<string, object?>(), userId);
scope.Complete();
return result;
}
@@ -3,7 +3,6 @@ using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.ContentEditing;
using Umbraco.Cms.Core.Models.ContentTypeEditing;
using Umbraco.Cms.Core.PropertyEditors;
using Umbraco.Cms.Core.Services.OperationStatus;
using Umbraco.Cms.Core.Strings;
using Umbraco.Extensions;
@@ -16,6 +15,7 @@ internal sealed class ContentTypeEditingService : ContentTypeEditingServiceBase<
{
private readonly ITemplateService _templateService;
private readonly IElementSwitchValidator _elementSwitchValidator;
private readonly IReservedFieldNamesService _reservedFieldNamesService;
private readonly IContentTypeService _contentTypeService;
public ContentTypeEditingService(
@@ -24,12 +24,33 @@ internal sealed class ContentTypeEditingService : ContentTypeEditingServiceBase<
IDataTypeService dataTypeService,
IEntityService entityService,
IShortStringHelper shortStringHelper,
IElementSwitchValidator elementSwitchValidator)
IElementSwitchValidator elementSwitchValidator,
IReservedFieldNamesService reservedFieldNamesService)
: base(contentTypeService, contentTypeService, dataTypeService, entityService, shortStringHelper)
{
_contentTypeService = contentTypeService;
_templateService = templateService;
_elementSwitchValidator = elementSwitchValidator;
_reservedFieldNamesService = reservedFieldNamesService;
}
[Obsolete("Use the constructor that is not marked obsolete, will be removed in v17")]
public ContentTypeEditingService(
IContentTypeService contentTypeService,
ITemplateService templateService,
IDataTypeService dataTypeService,
IEntityService entityService,
IShortStringHelper shortStringHelper,
IElementSwitchValidator elementSwitchValidator)
: this(
contentTypeService,
templateService,
dataTypeService,
entityService,
shortStringHelper,
elementSwitchValidator,
StaticServiceProvider.Instance.GetRequiredService<IReservedFieldNamesService>())
{
}
[Obsolete("Use the constructor that is not marked obsolete, will be removed in v16")]
@@ -39,11 +60,15 @@ internal sealed class ContentTypeEditingService : ContentTypeEditingServiceBase<
IDataTypeService dataTypeService,
IEntityService entityService,
IShortStringHelper shortStringHelper)
: base(contentTypeService, contentTypeService, dataTypeService, entityService, shortStringHelper)
: this(
contentTypeService,
templateService,
dataTypeService,
entityService,
shortStringHelper,
StaticServiceProvider.Instance.GetRequiredService<IElementSwitchValidator>(),
StaticServiceProvider.Instance.GetRequiredService<IReservedFieldNamesService>())
{
_contentTypeService = contentTypeService;
_templateService = templateService;
_elementSwitchValidator = StaticServiceProvider.Instance.GetRequiredService<IElementSwitchValidator>();
}
public async Task<Attempt<IContentType?, ContentTypeOperationStatus>> CreateAsync(ContentTypeCreateModel model, Guid userKey)
@@ -184,4 +209,6 @@ internal sealed class ContentTypeEditingService : ContentTypeEditingServiceBase<
protected override UmbracoObjectTypes ContentTypeObjectType => UmbracoObjectTypes.DocumentType;
protected override UmbracoObjectTypes ContainerObjectType => UmbracoObjectTypes.DocumentTypeContainer;
protected override ISet<string> GetReservedFieldNames() => _reservedFieldNamesService.GetDocumentReservedFieldNames();
}
@@ -1,6 +1,5 @@
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.ContentTypeEditing;
using Umbraco.Cms.Core.Models.PublishedContent;
using Umbraco.Cms.Core.Services.OperationStatus;
using Umbraco.Cms.Core.Strings;
using Umbraco.Extensions;
@@ -18,7 +17,6 @@ internal abstract class ContentTypeEditingServiceBase<TContentType, TContentType
private readonly IDataTypeService _dataTypeService;
private readonly IEntityService _entityService;
private readonly IShortStringHelper _shortStringHelper;
protected ContentTypeEditingServiceBase(
IContentTypeService contentTypeService,
TContentTypeService concreteContentTypeService,
@@ -91,7 +89,12 @@ internal abstract class ContentTypeEditingServiceBase<TContentType, TContentType
return Attempt.FailWithStatus<TContentType?, ContentTypeOperationStatus>(operationStatus, null);
}
await AdditionalCreateValidationAsync(model);
// perform additional, content type specific validation
operationStatus = await AdditionalCreateValidationAsync(model);
if (operationStatus is not ContentTypeOperationStatus.Success)
{
return Attempt.FailWithStatus<TContentType?, ContentTypeOperationStatus>(operationStatus, null);
}
// get the ID of the parent to create the content type under (we already validated that it exists)
var parentId = GetParentId(model, containerKey) ?? throw new ArgumentException("Parent ID could not be found", nameof(model));
@@ -414,13 +417,13 @@ internal abstract class ContentTypeEditingServiceBase<TContentType, TContentType
return reservedAliases.InvariantContains(alias);
}
protected abstract ISet<string> GetReservedFieldNames();
private bool ContainsReservedPropertyTypeAlias(ContentTypeEditingModelBase<TPropertyTypeModel, TPropertyTypeContainer> model)
{
// Because of models builder you cannot have an alias that already exists in IPublishedContent, for instance Path.
// Since MyModel.Path would conflict with IPublishedContent.Path.
var reservedPropertyTypeNames = typeof(IPublishedContent).GetPublicProperties().Select(x => x.Name)
.Union(typeof(IPublishedContent).GetPublicMethods().Select(x => x.Name))
.ToArray();
ISet<string> reservedPropertyTypeNames = GetReservedFieldNames();
return model.Properties.Any(propertyType => reservedPropertyTypeNames.InvariantContains(propertyType.Alias));
}
@@ -1,4 +1,6 @@
using Umbraco.Cms.Core.Media;
using Microsoft.Extensions.DependencyInjection;
using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Core.Media;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.ContentTypeEditing;
using Umbraco.Cms.Core.PropertyEditors;
@@ -13,7 +15,25 @@ internal sealed class MediaTypeEditingService : ContentTypeEditingServiceBase<IM
private readonly IMediaTypeService _mediaTypeService;
private readonly IDataTypeService _dataTypeService;
private readonly IImageUrlGenerator _imageUrlGenerator;
private readonly IReservedFieldNamesService _reservedFieldNamesService;
public MediaTypeEditingService(
IContentTypeService contentTypeService,
IMediaTypeService mediaTypeService,
IDataTypeService dataTypeService,
IEntityService entityService,
IShortStringHelper shortStringHelper,
IImageUrlGenerator imageUrlGenerator,
IReservedFieldNamesService reservedFieldNamesService)
: base(contentTypeService, mediaTypeService, dataTypeService, entityService, shortStringHelper)
{
_mediaTypeService = mediaTypeService;
_dataTypeService = dataTypeService;
_imageUrlGenerator = imageUrlGenerator;
_reservedFieldNamesService = reservedFieldNamesService;
}
[Obsolete("Use the non obsolete constructor instead. Scheduled for removal in v16")]
public MediaTypeEditingService(
IContentTypeService contentTypeService,
IMediaTypeService mediaTypeService,
@@ -26,6 +46,7 @@ internal sealed class MediaTypeEditingService : ContentTypeEditingServiceBase<IM
_mediaTypeService = mediaTypeService;
_dataTypeService = dataTypeService;
_imageUrlGenerator = imageUrlGenerator;
_reservedFieldNamesService = StaticServiceProvider.Instance.GetRequiredService<IReservedFieldNamesService>();
}
public async Task<Attempt<IMediaType?, ContentTypeOperationStatus>> CreateAsync(MediaTypeCreateModel model, Guid userKey)
@@ -145,6 +166,8 @@ internal sealed class MediaTypeEditingService : ContentTypeEditingServiceBase<IM
protected override UmbracoObjectTypes ContainerObjectType => UmbracoObjectTypes.MediaTypeContainer;
protected override ISet<string> GetReservedFieldNames() => _reservedFieldNamesService.GetMediaReservedFieldNames();
private async Task<IDictionary<IMediaType, IEnumerable<string>>> FetchAllowedFileExtensionsByMediaTypeAsync(IEnumerable<IMediaType> mediaTypes)
{
var allowedFileExtensionsByMediaType = new Dictionary<IMediaType, IEnumerable<string>>();
@@ -1,4 +1,6 @@
using Umbraco.Cms.Core.Models;
using Microsoft.Extensions.DependencyInjection;
using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.ContentTypeEditing;
using Umbraco.Cms.Core.Models.Membership;
using Umbraco.Cms.Core.Services.OperationStatus;
@@ -10,7 +12,24 @@ internal sealed class MemberTypeEditingService : ContentTypeEditingServiceBase<I
{
private readonly IMemberTypeService _memberTypeService;
private readonly IUserService _userService;
private readonly IReservedFieldNamesService _reservedFieldNamesService;
public MemberTypeEditingService(
IContentTypeService contentTypeService,
IMemberTypeService memberTypeService,
IDataTypeService dataTypeService,
IEntityService entityService,
IShortStringHelper shortStringHelper,
IUserService userService,
IReservedFieldNamesService reservedFieldNamesService)
: base(contentTypeService, memberTypeService, dataTypeService, entityService, shortStringHelper)
{
_memberTypeService = memberTypeService;
_userService = userService;
_reservedFieldNamesService = reservedFieldNamesService;
}
[Obsolete("Use the non obsolete constructor instead. Scheduled for removal in v16")]
public MemberTypeEditingService(
IContentTypeService contentTypeService,
IMemberTypeService memberTypeService,
@@ -22,6 +41,7 @@ internal sealed class MemberTypeEditingService : ContentTypeEditingServiceBase<I
{
_memberTypeService = memberTypeService;
_userService = userService;
_reservedFieldNamesService = StaticServiceProvider.Instance.GetRequiredService<IReservedFieldNamesService>();
}
public async Task<Attempt<IMemberType?, ContentTypeOperationStatus>> CreateAsync(MemberTypeCreateModel model, Guid userKey)
@@ -79,6 +99,8 @@ internal sealed class MemberTypeEditingService : ContentTypeEditingServiceBase<I
protected override UmbracoObjectTypes ContainerObjectType => throw new NotSupportedException("Member type tree does not support containers");
protected override ISet<string> GetReservedFieldNames() => _reservedFieldNamesService.GetMemberReservedFieldNames();
private void UpdatePropertyTypeVisibility(IMemberType memberType, MemberTypeModelBase model)
{
foreach (MemberTypePropertyTypeModel propertyType in model.Properties)
@@ -4,6 +4,7 @@ using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Core.Events;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Notifications;
using Umbraco.Cms.Core.Persistence.Querying;
using Umbraco.Cms.Core.Persistence.Repositories;
using Umbraco.Cms.Core.Scoping;
using Umbraco.Cms.Core.Services.Changes;
@@ -147,6 +148,16 @@ public class ContentTypeService : ContentTypeServiceBase<IContentTypeRepository,
}
}
public async Task<IEnumerable<IContentType>> GetByQueryAsync(IQuery<IContentType> query, CancellationToken cancellationToken)
{
using ICoreScope scope = ScopeProvider.CreateCoreScope();
// that one is special because it works across content, media and member types
scope.ReadLock(Constants.Locks.ContentTypes);
IEnumerable<IContentType> contentTypes = Repository.Get(query);
scope.Complete();
return contentTypes;
}
protected override void DeleteItemsOfTypes(IEnumerable<int> typeIds)
{
using (ICoreScope scope = ScopeProvider.CreateCoreScope())
@@ -1183,8 +1183,13 @@ public abstract class ContentTypeServiceBase<TRepository, TItem> : ContentTypeSe
return pagedModel;
}
/// <inheritdoc />
public async Task<Attempt<PagedModel<TItem>?, ContentTypeOperationStatus>> GetAllowedChildrenAsync(Guid key, int skip, int take)
=> await GetAllowedChildrenAsync(key, null, skip, take);
/// <inheritdoc />
public async Task<Attempt<PagedModel<TItem>?, ContentTypeOperationStatus>> GetAllowedChildrenAsync(Guid key, Guid? parentContentKey, int skip, int take)
{
using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true);
TItem? parent = Get(key);
@@ -1197,7 +1202,7 @@ public abstract class ContentTypeServiceBase<TRepository, TItem> : ContentTypeSe
IEnumerable<ContentTypeSort> allowedContentTypes = parent.AllowedContentTypes;
foreach (IContentTypeFilter filter in _contentTypeFilters)
{
allowedContentTypes = await filter.FilterAllowedChildrenAsync(allowedContentTypes, key);
allowedContentTypes = await filter.FilterAllowedChildrenAsync(allowedContentTypes, key, parentContentKey);
}
PagedModel<TItem> result;
File diff suppressed because it is too large Load Diff
@@ -13,13 +13,28 @@ public interface IContentTypeFilter
/// <param name="contentTypes">Retrieved collection of content types.</param>
/// <returns>Filtered collection of content types.</returns>
Task<IEnumerable<TItem>> FilterAllowedAtRootAsync<TItem>(IEnumerable<TItem> contentTypes)
where TItem : IContentTypeComposition;
where TItem : IContentTypeComposition
=> Task.FromResult(contentTypes);
/// <summary>
/// Filters the content types retrieved for being allowed as children of a parent content type.
/// </summary>
/// <param name="contentTypes">Retrieved collection of content types.</param>
/// <param name="parentKey">The parent content type key.</param>
/// <param name="parentKey">The parent content key.</param>
/// <returns>Filtered collection of content types.</returns>
Task<IEnumerable<ContentTypeSort>> FilterAllowedChildrenAsync(IEnumerable<ContentTypeSort> contentTypes, Guid parentKey);
[Obsolete("Please use the method overload taking all parameters. Scheduled for removal in Umbraco 17.")]
Task<IEnumerable<ContentTypeSort>> FilterAllowedChildrenAsync(IEnumerable<ContentTypeSort> contentTypes, Guid parentKey)
=> Task.FromResult(contentTypes);
/// <summary>
/// Filters the content types retrieved for being allowed as children of a parent content type.
/// </summary>
/// <param name="contentTypes">Retrieved collection of content types.</param>
/// <param name="parentContentTypeKey">The parent content type key.</param>
/// <param name="parentContentKey">The parent content key (provided to allow for custom filtering of the returned list of children based on the content context).</param>
/// <returns>Filtered collection of content types.</returns>
Task<IEnumerable<ContentTypeSort>> FilterAllowedChildrenAsync(IEnumerable<ContentTypeSort> contentTypes, Guid parentContentTypeKey, Guid? parentContentKey)
#pragma warning disable CS0618 // Type or member is obsolete
=> FilterAllowedChildrenAsync(contentTypes, parentContentTypeKey);
#pragma warning restore CS0618 // Type or member is obsolete
}
@@ -0,0 +1,8 @@
using Umbraco.Cms.Core.Models;
namespace Umbraco.Cms.Core.Services;
public interface IContentTypeSearchService
{
Task<PagedModel<IContentType>> SearchAsync(string query, bool? isElement, CancellationToken cancellationToken, int skip = 0, int take = 100);
}
@@ -1,4 +1,5 @@
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Persistence.Querying;
using Umbraco.Cms.Core.Services.OperationStatus;
namespace Umbraco.Cms.Core.Services;
@@ -30,4 +31,6 @@ public interface IContentTypeService : IContentTypeBaseService<IContentType>
/// <param name="aliases"></param>
/// <returns></returns>
IEnumerable<int> GetAllContentTypeIds(string[] aliases);
Task<IEnumerable<IContentType>> GetByQueryAsync(IQuery<IContentType> query, CancellationToken cancellationToken) => Task.FromResult(Enumerable.Empty<IContentType>());
}
@@ -159,12 +159,20 @@ public interface IContentTypeBaseService<TItem> : IContentTypeBaseService, IServ
/// <summary>
/// Returns all the content type allowed as root.
/// </summary>
/// <returns></returns>
Task<PagedModel<TItem>> GetAllAllowedAsRootAsync(int skip, int take);
/// <summary>
/// Returns all content types allowed as children for a given content type key.
/// </summary>
/// <returns></returns>
/// <param name="key">The content type key.</param>
Task<Attempt<PagedModel<TItem>?, ContentTypeOperationStatus>> GetAllowedChildrenAsync(Guid key, int skip, int take);
/// <summary>
/// Returns all content types allowed as children for a given content type key.
/// </summary>
/// <param name="key">The content type key.</param>
/// <param name="parentContentKey">The parent content key.</param>
Task<Attempt<PagedModel<TItem>?, ContentTypeOperationStatus>> GetAllowedChildrenAsync(Guid key, Guid? parentContentKey, int skip, int take)
=> GetAllowedChildrenAsync(key, skip, take);
}
@@ -1,38 +1,97 @@
using Umbraco.Cms.Core.Media.EmbedProviders;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Routing;
namespace Umbraco.Cms.Core.Services;
/// <summary>
/// Defines operations for handling document URLs.
/// </summary>
public interface IDocumentUrlService
{
/// <summary>
/// Initializes the service and ensure the content in the database is correct with the current configuration.
/// </summary>
/// <param name="forceEmpty"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
/// <param name="forceEmpty">Forces an early return when we know there are no routes (i.e. on install).</param>
/// <param name="cancellationToken">The cancellation token.</param>
Task InitAsync(bool forceEmpty, CancellationToken cancellationToken);
Task RebuildAllUrlsAsync();
/// <summary>
/// Gets the Url from a document key, culture and segment. Preview urls are returned if isPreview is true.
/// Rebuilds all document URLs.
/// </summary>
Task RebuildAllUrlsAsync();
/// <summary>
/// Gets a single URL segment from a document key and culture. Preview urls are returned if isDraft is true.
/// </summary>
/// <param name="documentKey">The key of the document.</param>
/// <param name="culture">The culture code.</param>
/// <param name="isDraft">Whether to get the url of the draft or published document.</param>
/// <returns>The url of the document.</returns>
/// <returns>A URL segment for the document.</returns>
/// <remarks>If more than one segment is available, the first retrieved and indicated as primary will be returned.</remarks>
string? GetUrlSegment(Guid documentKey, string culture, bool isDraft);
/// <summary>
/// Gets the URL segments from a document key and culture. Preview urls are returned if isDraft is true.
/// </summary>
/// <param name="documentKey">The key of the document.</param>
/// <param name="culture">The culture code.</param>
/// <param name="isDraft">Whether to get the url of the draft or published document.</param>
/// <returns>The URL segments for the document.</returns>
IEnumerable<string> GetUrlSegments(Guid documentKey, string culture, bool isDraft)
=> throw new NotImplementedException();
/// <summary>
/// Creates or updates the URL segments for a single document.
/// </summary>
/// <param name="key">The document key.</param>
Task CreateOrUpdateUrlSegmentsAsync(Guid key);
/// <summary>
/// Creates or updates the URL segments for a document and it's descendants.
/// </summary>
/// <param name="key">The document key.</param>
Task CreateOrUpdateUrlSegmentsWithDescendantsAsync(Guid key);
/// <summary>
/// Creates or updates the URL segments for a collection of documents.
/// </summary>
/// <param name="documents">The document collection.</param>
Task CreateOrUpdateUrlSegmentsAsync(IEnumerable<IContent> documents);
/// <summary>
/// Deletes all URLs from the cache for a collection of document keys.
/// </summary>
/// <param name="documentKeys">The collection of document keys.</param>
Task DeleteUrlsFromCacheAsync(IEnumerable<Guid> documentKeys);
/// <summary>
/// Gets a document key by route.
/// </summary>
/// <param name="route">The route.</param>
/// <param name="culture">The culture code.</param>
/// <param name="documentStartNodeId">The document start node Id.</param>
/// <param name="isDraft">Whether to get the url of the draft or published document.</param>
/// <returns>The document key, or null if not found.</returns>
Guid? GetDocumentKeyByRoute(string route, string? culture, int? documentStartNodeId, bool isDraft);
/// <summary>
/// Gets all the URLs for a given content key.
/// </summary>
/// <param name="contentKey">The content key.</param>
[Obsolete("This method is obsolete and will be removed in future versions. Use IPublishedUrlInfoProvider.GetAllAsync instead. Scheduled for removal in Umbraco 17.")]
Task<IEnumerable<UrlInfo>> ListUrlsAsync(Guid contentKey);
Task CreateOrUpdateUrlSegmentsWithDescendantsAsync(Guid key);
Task CreateOrUpdateUrlSegmentsAsync(Guid key);
/// <summary>
/// Gets the legacy route format for a document key and culture.
/// </summary>
/// <param name="key">The key of the document.</param>
/// <param name="culture">The culture code.</param>
/// <param name="isDraft">Whether to get the url of the draft or published document.</param>
/// <returns></returns>
string GetLegacyRouteFormat(Guid key, string? culture, bool isDraft);
/// <summary>
/// Gets a value indicating whether any URLs have been cached.
/// </summary>
bool HasAny();
}
@@ -20,7 +20,19 @@ public interface IIndexedEntitySearchService
PagedModel<IEntitySlim> Search(UmbracoObjectTypes objectType, string query, Guid? parentId, int skip = 0, int take = 100, bool ignoreUserStartNodes = false)
=> Search(objectType,query, skip, take, ignoreUserStartNodes);
// default implementation to avoid breaking changes falls back to old behaviour
[Obsolete("Please use the method that accepts all parameters. Will be removed in V17.")]
PagedModel<IEntitySlim> Search(UmbracoObjectTypes objectType, string query, Guid? parentId, IEnumerable<Guid>? contentTypeIds, int skip = 0, int take = 100, bool ignoreUserStartNodes = false)
=> Search(objectType,query, skip, take, ignoreUserStartNodes);
// default implementation to avoid breaking changes falls back to old behaviour
PagedModel<IEntitySlim> Search(
UmbracoObjectTypes objectType,
string query,
Guid? parentId,
IEnumerable<Guid>? contentTypeIds,
bool? trashed,
int skip = 0,
int take = 100,
bool ignoreUserStartNodes = false)
=> Search(objectType,query, skip, take, ignoreUserStartNodes);
}
@@ -29,7 +29,7 @@ public interface ITwoFactorLoginService : IService
/// The returned type can be anything depending on the setup providers. You will need to cast it to the type handled by
/// the provider.
/// </remarks>
[Obsolete("Use IUserTwoFactorLoginService.GetSetupInfoWithStatusAsync. This will be removed in Umbraco 15.")]
[Obsolete("Use IUserTwoFactorLoginService.GetSetupInfoAsync. This will be removed in Umbraco 15.")]
Task<object?> GetSetupInfoAsync(Guid userOrMemberKey, string providerName);
/// <summary>
@@ -60,13 +60,13 @@ public interface ITwoFactorLoginService : IService
/// <summary>
/// Disables 2FA with Code.
/// </summary>
[Obsolete("Use IUserTwoFactorLoginService.DisableByCodeWithStatusAsync. This will be removed in Umbraco 15.")]
[Obsolete("Use IUserTwoFactorLoginService.DisableByCodeAsync. This will be removed in Umbraco 15.")]
Task<bool> DisableWithCodeAsync(string providerName, Guid userOrMemberKey, string code);
/// <summary>
/// Validates and Saves.
/// </summary>
[Obsolete("Use IUserTwoFactorLoginService.ValidateAndSaveWithStatusAsync. This will be removed in Umbraco 15.")]
[Obsolete("Use IUserTwoFactorLoginService.ValidateAndSaveAsync. This will be removed in Umbraco 15.")]
Task<bool> ValidateAndSaveAsync(string providerName, Guid userKey, string secret, string code);
}
@@ -1,4 +1,4 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations;
using Microsoft.Extensions.DependencyInjection;
using Umbraco.Cms.Core.Cache;
using Umbraco.Cms.Core.DependencyInjection;
@@ -88,7 +88,7 @@ public class PropertyValidationService : IPropertyValidationService
{
// Retrieve default messages used for required and regex validatation. We'll replace these
// if set with custom ones if they've been provided for a given property.
var requiredDefaultMessages = new[] { Constants.Validation.ErrorMessages.Properties.Missing };
var requiredDefaultMessages = new[] { Constants.Validation.ErrorMessages.Properties.Missing, Constants.Validation.ErrorMessages.Properties.Empty };
var formatDefaultMessages = new[] { Constants.Validation.ErrorMessages.Properties.PatternMismatch };
IDataValueEditor valueEditor = _valueEditorCache.GetValueEditor(editor, dataType);
+1 -1
View File
@@ -2680,7 +2680,7 @@ internal partial class UserService : RepositoryService, IUserService
}
}
[GeneratedRegex(@"^[\w\d\-\._~]{1,255}$")]
[GeneratedRegex(@"^[\w\d\-\._~]{1,100}$")]
private static partial Regex ValidClientId();
#endregion
@@ -8,6 +8,16 @@ namespace Umbraco.Cms.Core.Strings;
/// <remarks>Url segments should comply with IETF RFCs regarding content, encoding, etc.</remarks>
public interface IUrlSegmentProvider
{
/// <summary>
/// Gets a value indicating whether the URL segment provider allows additional segments after providing one.
/// </summary>
/// <remarks>
/// If set to true, when more than one URL segment provider is available, futher providers after this one in the collection will be called
/// even if the current provider provides a segment.
/// If false, the provider will terminate the chain of URL segment providers if it provides a segment.
/// </remarks>
bool AllowAdditionalSegments => false;
/// <summary>
/// Gets the URL segment for a specified content and culture.
/// </summary>
@@ -20,6 +30,18 @@ public interface IUrlSegmentProvider
/// URL per culture.
/// </remarks>
string? GetUrlSegment(IContentBase content, string? culture = null);
/// <summary>
/// Gets the URL segment for a specified content, published status and and culture.
/// </summary>
/// <param name="content">The content.</param>
/// <param name="culture">The culture.</param>
/// <returns>The URL segment.</returns>
/// <remarks>
/// This is for when Umbraco is capable of managing more than one URL
/// per content, in 1-to-1 multilingual configurations. Then there would be one
/// URL per culture.
/// </remarks>
string? GetUrlSegment(IContentBase content, bool published, string? culture = null) => GetUrlSegment(content, culture);
// TODO: For the 301 tracking, we need to add another extended interface to this so that
@@ -1,6 +1,7 @@
// Copyright (c) Umbraco.
// See LICENSE for more details.
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Text;
using System.Text.RegularExpressions;
@@ -61,7 +62,19 @@ public class BackOfficeExamineSearcher : IBackOfficeExamineSearcher
out long totalFound,
string? searchFrom = null,
bool ignoreUserStartNodes = false)
=> Search(query, entityType, pageSize, pageIndex, out totalFound, null, searchFrom, ignoreUserStartNodes);
=> Search(query, entityType, pageSize, pageIndex, out totalFound, null, null, searchFrom, ignoreUserStartNodes);
[Obsolete("Please use the method that accepts all parameters. Will be removed in V17.")]
public IEnumerable<ISearchResult> Search(
string query,
UmbracoEntityTypes entityType,
int pageSize,
long pageIndex,
out long totalFound,
string[]? contentTypeAliases,
string? searchFrom = null,
bool ignoreUserStartNodes = false)
=> Search(query, entityType, pageSize, pageIndex, out totalFound, contentTypeAliases, null, searchFrom, ignoreUserStartNodes);
public IEnumerable<ISearchResult> Search(
string query,
@@ -70,6 +83,7 @@ public class BackOfficeExamineSearcher : IBackOfficeExamineSearcher
long pageIndex,
out long totalFound,
string[]? contentTypeAliases,
bool? trashed,
string? searchFrom = null,
bool ignoreUserStartNodes = false)
{
@@ -133,6 +147,12 @@ public class BackOfficeExamineSearcher : IBackOfficeExamineSearcher
? currentUser.CalculateMediaStartNodeIds(_entityService, _appCaches)
: Array.Empty<int>();
AppendPath(sb, UmbracoObjectTypes.Media, allMediaStartNodes, searchFrom, ignoreUserStartNodes, _entityService);
if (trashed.HasValue)
{
AppendRequiredTrashPath(trashed.Value, sb, Constants.System.RecycleBinMedia);
}
break;
case UmbracoEntityTypes.Document:
type = "content";
@@ -146,6 +166,12 @@ public class BackOfficeExamineSearcher : IBackOfficeExamineSearcher
? currentUser.CalculateContentStartNodeIds(_entityService, _appCaches)
: Array.Empty<int>();
AppendPath(sb, UmbracoObjectTypes.Document, allContentStartNodes, searchFrom, ignoreUserStartNodes, _entityService);
if (trashed.HasValue)
{
AppendRequiredTrashPath(trashed.Value, sb, Constants.System.RecycleBinContent);
}
break;
default:
throw new NotSupportedException("The " + typeof(BackOfficeExamineSearcher) +
@@ -181,6 +207,14 @@ public class BackOfficeExamineSearcher : IBackOfficeExamineSearcher
return result;
}
private void AppendRequiredTrashPath(bool trashed, StringBuilder sb, int recycleBinId)
{
var requiredOrNotString = trashed ? "+" : "!";
var trashPath = $"-1,{recycleBinId}";
trashPath = trashPath.Replace("-", "\\-").Replace(",", "\\,");
sb.Append($"{requiredOrNotString}__Path:{trashPath}\\,* ");
}
private bool BuildQuery(StringBuilder sb, string query, string? searchFrom, List<string> fields, string type)
{
//build a lucene query:
@@ -394,6 +428,7 @@ public class BackOfficeExamineSearcher : IBackOfficeExamineSearcher
searchFromId > 0
? entityService.GetAllPaths(objectType, searchFromId).FirstOrDefault()
: null;
if (entityPath != null)
{
// find... only what's underneath
@@ -78,6 +78,7 @@ public static partial class UmbracoBuilderExtensions
builder.Services.AddUnique<IContentListViewService, ContentListViewService>();
builder.Services.AddUnique<IMediaListViewService, MediaListViewService>();
builder.Services.AddUnique<IEntitySearchService, EntitySearchService>();
builder.Services.AddUnique<IContentTypeSearchService, ContentTypeSearchService>();
builder.Services.AddUnique<IIndexedEntitySearchService, IndexedEntitySearchService>();
builder.Services.TryAddTransient<IReservedFieldNamesService, ReservedFieldNamesService>();
@@ -18,7 +18,7 @@ public interface IBackOfficeExamineSearcher
string? searchFrom = null,
bool ignoreUserStartNodes = false);
// default implementation to avoid breaking changes falls back to old behaviour
[Obsolete("Please use the method that accepts all parameters. Will be removed in V17.")]
IEnumerable<ISearchResult> Search(
string query,
UmbracoEntityTypes entityType,
@@ -28,5 +28,18 @@ public interface IBackOfficeExamineSearcher
string[]? contentTypeAliases,
string? searchFrom = null,
bool ignoreUserStartNodes = false)
=> Search(query, entityType, pageSize, pageIndex, out totalFound, searchFrom, ignoreUserStartNodes);
// default implementation to avoid breaking changes falls back to old behaviour
IEnumerable<ISearchResult> Search(
string query,
UmbracoEntityTypes entityType,
int pageSize,
long pageIndex,
out long totalFound,
string[]? contentTypeAliases,
bool? trashed,
string? searchFrom = null,
bool ignoreUserStartNodes = false)
=> Search(query, entityType, pageSize, pageIndex, out totalFound, null, searchFrom, ignoreUserStartNodes);
}
@@ -70,6 +70,11 @@ public abstract class MigrationExpressionBase : IMigrationExpression
}
else
{
if (stmtBuilder.Length > 0)
{
stmtBuilder.Append(Environment.NewLine);
}
stmtBuilder.Append(line);
}
}
@@ -333,7 +333,7 @@ public class MigrationPlanExecutor : IMigrationPlanExecutor
{
_appCaches.RuntimeCache.Clear();
_appCaches.IsolatedCaches.ClearAllCaches();
_databaseCacheRebuilder.Rebuild();
_databaseCacheRebuilder.Rebuild(false);
_distributedCache.RefreshAllPublishedSnapshot();
}
@@ -7,6 +7,7 @@ namespace Umbraco.Cms.Infrastructure.Migrations.PostMigrations;
/// <summary>
/// Implements <see cref="ICacheRebuilder" /> in Umbraco.Web (rebuilding).
/// </summary>
[Obsolete("This is no longer used. Scheduled for removal in Umbraco 17.")]
public class CacheRebuilder : ICacheRebuilder
{
private readonly DistributedCache _distributedCache;
@@ -26,7 +27,7 @@ public class CacheRebuilder : ICacheRebuilder
/// <inheritdoc />
public void Rebuild()
{
_databaseCacheRebuilder.Rebuild();
_databaseCacheRebuilder.Rebuild(false);
_distributedCache.RefreshAllPublishedSnapshot();
}
}
@@ -10,6 +10,7 @@ namespace Umbraco.Cms.Infrastructure.Migrations.PostMigrations;
/// be refactored, really.
/// </para>
/// </remarks>
[Obsolete("This is no longer used. Scheduled for removal in Umbraco 17.")]
public interface ICacheRebuilder
{
/// <summary>
@@ -111,5 +111,8 @@ public class UmbracoPlan : MigrationPlan
// To 15.3.0
To<V_15_3_0.AddNameAndDescriptionToWebhooks>("{7B11F01E-EE33-4B0B-81A1-F78F834CA45B}");
// To 15.4.0
To<V_15_4_0.UpdateDocumentUrlToPersistMultipleSegmentsPerDocument>("{A9E72794-4036-4563-B543-1717C73B8879}");
}
}
@@ -28,7 +28,7 @@ public class AddNameAndDescriptionToWebhooks : MigrationBase
}
else
{
Logger.LogWarning($"Table {Constants.DatabaseSchema.Tables.Webhook} does not exist so the addition of the name and description by columnss in migration {nameof(AddNameAndDescriptionToWebhooks)} cannot be completed.");
Logger.LogWarning($"Table {Constants.DatabaseSchema.Tables.Webhook} does not exist so the addition of the name and description by columns in migration {nameof(AddNameAndDescriptionToWebhooks)} cannot be completed.");
}
}
@@ -0,0 +1,63 @@
using Microsoft.Extensions.Logging;
using Umbraco.Cms.Core;
using Umbraco.Cms.Infrastructure.Persistence.Dtos;
namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_15_4_0;
/// <summary>
/// Migration to make necessary schema updates to support multiple segments per document.
/// </summary>
public class UpdateDocumentUrlToPersistMultipleSegmentsPerDocument : MigrationBase
{
/// <summary>
/// Initializes a new instance of the <see cref="UpdateDocumentUrlToPersistMultipleSegmentsPerDocument"/> class.
/// </summary>
public UpdateDocumentUrlToPersistMultipleSegmentsPerDocument(IMigrationContext context)
: base(context)
{
}
/// <inheritdoc/>
protected override void Migrate()
{
Logger.LogDebug("Schema updates to {TableName} for support of multiple segments per document", Constants.DatabaseSchema.Tables.DocumentUrl);
if (TableExists(Constants.DatabaseSchema.Tables.DocumentUrl))
{
ExtendUniqueIndexAcrossSegmentField();
AddAndPopulateIsPrimaryColumn();
}
else
{
throw new InvalidOperationException(
$"Table {Constants.DatabaseSchema.Tables.DocumentUrl} does not exist so the migration {nameof(UpdateDocumentUrlToPersistMultipleSegmentsPerDocument)} could not be completed.");
}
}
private void ExtendUniqueIndexAcrossSegmentField()
{
Logger.LogDebug("Extending the unique index on {TableName} to include the urlSegment column", Constants.DatabaseSchema.Tables.DocumentUrl);
var indexName = "IX_" + Constants.DatabaseSchema.Tables.DocumentUrl;
if (IndexExists(indexName))
{
DeleteIndex<DocumentUrlDto>(indexName);
}
CreateIndex<DocumentUrlDto>(indexName);
}
private void AddAndPopulateIsPrimaryColumn()
{
const string IsPrimaryColumnName = "isPrimary";
Logger.LogDebug("Adding the {Column} column {TableName} to include the urlSegment column", IsPrimaryColumnName, Constants.DatabaseSchema.Tables.DocumentUrl);
var columns = Context.SqlContext.SqlSyntax.GetColumnsInSchema(Context.Database).ToList();
if (columns
.SingleOrDefault(x => x.TableName == Constants.DatabaseSchema.Tables.DocumentUrl && x.ColumnName == IsPrimaryColumnName) is null)
{
AddColumn<DocumentUrlDto>(Constants.DatabaseSchema.Tables.DocumentUrl, IsPrimaryColumnName);
}
}
}
@@ -51,7 +51,15 @@ public class TextBuilder : Builder
/// Outputs an "auto-generated" header to a string builder.
/// </summary>
/// <param name="sb">The string builder.</param>
public static void WriteHeader(StringBuilder sb) => TextHeaderWriter.WriteHeader(sb);
[Obsolete("Please use the overload taking all parameters. Scheduled for removal in Umbraco 17.")]
public static void WriteHeader(StringBuilder sb) => WriteHeader(sb, true);
/// <summary>
/// Outputs an "auto-generated" header to a string builder.
/// </summary>
/// <param name="sb">The string builder.</param>
/// <param name="includeVersion">Flag indicating whether the tool version number should be included in the output.</param>
public static void WriteHeader(StringBuilder sb, bool includeVersion) => TextHeaderWriter.WriteHeader(sb, includeVersion);
/// <summary>
/// Outputs a generated model to a string builder.
@@ -60,7 +68,7 @@ public class TextBuilder : Builder
/// <param name="typeModel">The model to generate.</param>
public void Generate(StringBuilder sb, TypeModel typeModel)
{
WriteHeader(sb);
WriteHeader(sb, Config.IncludeVersionNumberInGeneratedModels);
foreach (var t in TypesUsing)
{
@@ -83,7 +91,7 @@ public class TextBuilder : Builder
/// <param name="typeModels">The models to generate.</param>
public void Generate(StringBuilder sb, IEnumerable<TypeModel> typeModels)
{
WriteHeader(sb);
WriteHeader(sb, Config.IncludeVersionNumberInGeneratedModels);
foreach (var t in TypesUsing)
{
@@ -8,13 +8,30 @@ internal static class TextHeaderWriter
/// Outputs an "auto-generated" header to a string builder.
/// </summary>
/// <param name="sb">The string builder.</param>
public static void WriteHeader(StringBuilder sb)
[Obsolete("Please use the overload taking all parameters. Scheduled for removal in Umbraco 17.")]
public static void WriteHeader(StringBuilder sb) => WriteHeader(sb, true);
/// <summary>
/// Outputs an "auto-generated" header to a string builder.
/// </summary>
/// <param name="sb">The string builder.</param>
/// <param name="includeVersion">Flag indicating whether the tool version number should be included in the output.</param>
public static void WriteHeader(StringBuilder sb, bool includeVersion)
{
sb.Append("//------------------------------------------------------------------------------\n");
sb.Append("// <auto-generated>\n");
sb.Append("// This code was generated by a tool.\n");
sb.Append("//\n");
sb.AppendFormat("// Umbraco.ModelsBuilder.Embedded v{0}\n", ApiVersion.Current.Version);
if (includeVersion)
{
sb.AppendFormat("// Umbraco.ModelsBuilder.Embedded v{0}\n", ApiVersion.Current.Version);
}
else
{
sb.Append("// Umbraco.ModelsBuilder.Embedded\n");
}
sb.Append("//\n");
sb.Append("// Changes to this file will be lost if the code is regenerated.\n");
sb.Append("// </auto-generated>\n");
@@ -0,0 +1,37 @@
using Microsoft.Extensions.Options;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.PublishedContent;
using Umbraco.Extensions;
namespace Umbraco.Cms.Infrastructure.ModelsBuilder.Options;
public class ConfigurePropertySettingsOptions :
IConfigureOptions<ContentPropertySettings>,
IConfigureOptions<MemberPropertySettings>,
IConfigureOptions<MediaPropertySettings>
{
public void Configure(ContentPropertySettings options)
{
var reservedProperties = typeof(IPublishedContent).GetPublicProperties().Select(x => x.Name).ToHashSet();
var reservedMethods = typeof(IPublishedContent).GetPublicMethods().Select(x => x.Name).ToHashSet();
options.AddReservedFieldNames(reservedProperties);
options.AddReservedFieldNames(reservedMethods);
}
public void Configure(MemberPropertySettings options)
{
var reservedProperties = typeof(IPublishedMember).GetPublicProperties().Select(x => x.Name).ToHashSet();
var reservedMethods = typeof(IPublishedMember).GetPublicMethods().Select(x => x.Name).ToHashSet();
options.AddReservedFieldNames(reservedProperties);
options.AddReservedFieldNames(reservedMethods);
}
public void Configure(MediaPropertySettings options)
{
var reservedProperties = typeof(IPublishedContent).GetPublicProperties().Select(x => x.Name).ToHashSet();
var reservedMethods = typeof(IPublishedContent).GetPublicMethods().Select(x => x.Name).ToHashSet();
options.AddReservedFieldNames(reservedProperties);
options.AddReservedFieldNames(reservedMethods);
}
}
@@ -16,7 +16,7 @@ public class DocumentUrlDto
[PrimaryKeyColumn(Clustered = false, AutoIncrement = true)]
public int NodeId { get; set; }
[Index(IndexTypes.UniqueClustered, ForColumns = "uniqueId, languageId, isDraft", Name = "IX_" + TableName)]
[Index(IndexTypes.UniqueClustered, ForColumns = "uniqueId, languageId, isDraft, urlSegment", Name = "IX_" + TableName)]
[Column("uniqueId")]
[ForeignKey(typeof(NodeDto), Column = "uniqueId")]
public Guid UniqueId { get; set; }
@@ -28,14 +28,11 @@ public class DocumentUrlDto
[ForeignKey(typeof(LanguageDto))]
public int LanguageId { get; set; }
//
// [Column("segment")]
// [NullSetting(NullSetting = NullSettings.Null)]
// [Length(PropertyDataDto.SegmentLength)]
// public string Segment { get; set; } = string.Empty;
[Column("urlSegment")]
[NullSetting(NullSetting = NullSettings.NotNull)]
public string UrlSegment { get; set; } = string.Empty;
[Column("isPrimary")]
[Constraint(Default = 1)]
public bool IsPrimary { get; set; }
}
@@ -1,18 +1,11 @@
using Microsoft.Extensions.Logging;
using NPoco;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Cache;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.Entities;
using Umbraco.Cms.Core.Persistence.Querying;
using Umbraco.Cms.Core.Persistence.Repositories;
using Umbraco.Cms.Core.Security;
using Umbraco.Cms.Infrastructure.Persistence.Dtos;
using Umbraco.Cms.Infrastructure.Persistence.Factories;
using Umbraco.Cms.Infrastructure.Scoping;
using Umbraco.Extensions;
namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement;
public class DocumentUrlRepository : IDocumentUrlRepository
@@ -36,14 +29,15 @@ public class DocumentUrlRepository : IDocumentUrlRepository
public void Save(IEnumerable<PublishedDocumentUrlSegment> publishedDocumentUrlSegments)
{
//TODO avoid this is called as first thing on first restart after install
// TODO: avoid this is called as first thing on first restart after install
IEnumerable<Guid> documentKeys = publishedDocumentUrlSegments.Select(x => x.DocumentKey).Distinct();
Dictionary<(Guid UniqueId, int LanguageId, bool isDraft), DocumentUrlDto> dtoDictionary = publishedDocumentUrlSegments.Select(BuildDto).ToDictionary(x=> (x.UniqueId, x.LanguageId, x.IsDraft));
Dictionary<(Guid UniqueId, int LanguageId, bool isDraft, string urlSegment), DocumentUrlDto> dtoDictionary = publishedDocumentUrlSegments
.Select(BuildDto)
.ToDictionary(x => (x.UniqueId, x.LanguageId, x.IsDraft, x.UrlSegment));
var toUpdate = new List<DocumentUrlDto>();
var toDelete = new List<int>();
var toInsert = dtoDictionary.Values.ToDictionary(x => (x.UniqueId, x.LanguageId, x.IsDraft));
var toInsert = dtoDictionary.Values.ToDictionary(x => (x.UniqueId, x.LanguageId, x.IsDraft, x.UrlSegment));
foreach (IEnumerable<Guid> group in documentKeys.InGroupsOf(Constants.Sql.MaxParameterCount))
{
@@ -57,18 +51,12 @@ public class DocumentUrlRepository : IDocumentUrlRepository
foreach (DocumentUrlDto existing in existingUrlsInBatch)
{
if (dtoDictionary.TryGetValue((existing.UniqueId, existing.LanguageId, existing.IsDraft), out DocumentUrlDto? found))
if (dtoDictionary.TryGetValue((existing.UniqueId, existing.LanguageId, existing.IsDraft, existing.UrlSegment), out DocumentUrlDto? found))
{
found.NodeId = existing.NodeId;
// Only update if the url segment is different
if (found.UrlSegment != existing.UrlSegment)
{
toUpdate.Add(found);
}
// if we found it, we know we should not insert it as a new
toInsert.Remove((found.UniqueId, found.LanguageId, found.IsDraft));
// If we found it, we know we should not insert it as a new record.
toInsert.Remove((found.UniqueId, found.LanguageId, found.IsDraft, found.UrlSegment));
}
else
{
@@ -83,14 +71,6 @@ public class DocumentUrlRepository : IDocumentUrlRepository
Database.DeleteMany<DocumentUrlDto>().Where(x => toDelete.Contains(x.NodeId)).Execute();
}
if (toUpdate.Any())
{
foreach (DocumentUrlDto updated in toUpdate)
{
Database.Update(updated);
}
}
Database.InsertBulk(toInsert.Values);
}
@@ -115,7 +95,8 @@ public class DocumentUrlRepository : IDocumentUrlRepository
UrlSegment = dto.UrlSegment,
DocumentKey = dto.UniqueId,
LanguageId = dto.LanguageId,
IsDraft = dto.IsDraft
IsDraft = dto.IsDraft,
IsPrimary = dto.IsPrimary
};
private DocumentUrlDto BuildDto(PublishedDocumentUrlSegment model)
@@ -126,6 +107,7 @@ public class DocumentUrlRepository : IDocumentUrlRepository
UniqueId = model.DocumentKey,
LanguageId = model.LanguageId,
IsDraft = model.IsDraft,
IsPrimary = model.IsPrimary,
};
}
}
@@ -1,4 +1,4 @@
// Copyright (c) Umbraco.
// Copyright (c) Umbraco.
// See LICENSE for more details.
using System.ComponentModel.DataAnnotations;
@@ -16,12 +16,22 @@ internal abstract class BlockEditorMinMaxValidatorBase<TValue, TLayout> : IValue
where TValue : BlockValue<TLayout>, new()
where TLayout : class, IBlockLayoutItem, new()
{
/// <summary>
/// Initializes a new instance of the <see cref="BlockEditorMinMaxValidatorBase{TValue, TLayout}"/> class.
/// </summary>
protected BlockEditorMinMaxValidatorBase(ILocalizedTextService textService) => TextService = textService;
/// <summary>
/// Gets the <see cref="ILocalizedTextService"/>
/// </summary>
protected ILocalizedTextService TextService { get; }
/// <inheritdoc/>
public abstract IEnumerable<ValidationResult> Validate(object? value, string? valueType, object? dataTypeConfiguration, PropertyValidationContext validationContext);
/// <summary>
/// Validates the number of blocks are within the configured minimum and maximum values.
/// </summary>
protected IEnumerable<ValidationResult> ValidateNumberOfBlocks(BlockEditorData<TValue, TLayout>? blockEditorData, int? min, int? max)
{
var numberOfBlocks = blockEditorData?.Layout?.Count() ?? 0;
@@ -35,8 +45,8 @@ internal abstract class BlockEditorMinMaxValidatorBase<TValue, TLayout> : IValue
TextService.Localize(
"validation",
"entriesShort",
new[] { min.ToString(), (min - numberOfBlocks).ToString(), }),
new[] { "minCount" });
[min.ToString(), (min - numberOfBlocks).ToString(),]),
["value"]);
}
}
@@ -46,8 +56,8 @@ internal abstract class BlockEditorMinMaxValidatorBase<TValue, TLayout> : IValue
TextService.Localize(
"validation",
"entriesExceed",
new[] { max.ToString(), (numberOfBlocks - max).ToString(), }),
new[] { "maxCount" });
[max.ToString(), (numberOfBlocks - max).ToString(),]),
["value"]);
}
}
}
@@ -20,8 +20,6 @@ public abstract class BlockEditorPropertyValueEditor<TValue, TLayout> : BlockVal
where TValue : BlockValue<TLayout>, new()
where TLayout : class, IBlockLayoutItem, new()
{
private readonly IJsonSerializer _jsonSerializer;
[Obsolete("Please use the non-obsolete constructor. Will be removed in V16.")]
protected BlockEditorPropertyValueEditor(
DataEditorAttribute attribute,
@@ -52,7 +50,12 @@ public abstract class BlockEditorPropertyValueEditor<TValue, TLayout> : BlockVal
IIOHelper ioHelper,
DataEditorAttribute attribute)
: base(propertyEditors, dataTypeConfigurationCache, shortStringHelper, jsonSerializer, dataValueReferenceFactories, blockEditorVarianceHandler, languageService, ioHelper, attribute) =>
_jsonSerializer = jsonSerializer;
JsonSerializer = jsonSerializer;
/// <summary>
/// Gets the <see cref="IJsonSerializer"/>.
/// </summary>
protected IJsonSerializer JsonSerializer { get; }
/// <inheritdoc />
public override IEnumerable<UmbracoEntityReference> GetReferences(object? value)
@@ -143,6 +146,6 @@ public abstract class BlockEditorPropertyValueEditor<TValue, TLayout> : BlockVal
MapBlockValueFromEditor(blockEditorData.BlockValue);
// return json
return _jsonSerializer.Serialize(blockEditorData.BlockValue);
return JsonSerializer.Serialize(blockEditorData.BlockValue);
}
}
@@ -9,7 +9,7 @@ using Umbraco.Extensions;
namespace Umbraco.Cms.Core.PropertyEditors;
/// <summary>
/// Represents a block list property editor.
/// Represents a block list property editor.
/// </summary>
[DataEditor(
Constants.PropertyEditors.Aliases.BlockList,
@@ -19,6 +19,9 @@ public class BlockListPropertyEditor : BlockListPropertyEditorBase
{
private readonly IIOHelper _ioHelper;
/// <summary>
/// Initializes a new instance of the <see cref="BlockListPropertyEditor"/> class.
/// </summary>
public BlockListPropertyEditor(
IDataValueEditorFactory dataValueEditorFactory,
IIOHelper ioHelper,
@@ -27,6 +30,9 @@ public class BlockListPropertyEditor : BlockListPropertyEditorBase
: base(dataValueEditorFactory, blockValuePropertyIndexValueFactory, jsonSerializer)
=> _ioHelper = ioHelper;
/// <summary>
/// Initializes a new instance of the <see cref="BlockListPropertyEditor"/> class.
/// </summary>
[Obsolete("Use constructor that doesn't take PropertyEditorCollection, scheduled for removal in V15")]
public BlockListPropertyEditor(
IDataValueEditorFactory dataValueEditorFactory,
@@ -38,6 +44,7 @@ public class BlockListPropertyEditor : BlockListPropertyEditorBase
{
}
/// <inheritdoc/>
public override bool SupportsConfigurableElements => true;
/// <inheritdoc />
@@ -50,6 +57,7 @@ public class BlockListPropertyEditor : BlockListPropertyEditorBase
return valueEditor.MergePartialPropertyValueForCulture(sourceValue, targetValue, culture);
}
/// <inheritdoc/>
public override object? MergeVariantInvariantPropertyValue(
object? sourceValue,
object? targetValue,
@@ -60,10 +68,7 @@ public class BlockListPropertyEditor : BlockListPropertyEditorBase
return valueEditor.MergeVariantInvariantPropertyValue(sourceValue, targetValue, canUpdateInvariantData, allowedCultures);
}
#region Pre Value Editor
/// <inheritdoc/>
protected override IConfigurationEditor CreateConfigurationEditor() =>
new BlockListConfigurationEditor(_ioHelper);
#endregion
}
@@ -11,6 +11,7 @@ using Umbraco.Cms.Core.PropertyEditors.ValueConverters;
using Umbraco.Cms.Core.Serialization;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Strings;
using Umbraco.Cms.Infrastructure.PropertyEditors.Validators;
using StaticServiceProvider = Umbraco.Cms.Core.DependencyInjection.StaticServiceProvider;
namespace Umbraco.Cms.Core.PropertyEditors;
@@ -23,12 +24,18 @@ public abstract class BlockListPropertyEditorBase : DataEditor
private readonly IBlockValuePropertyIndexValueFactory _blockValuePropertyIndexValueFactory;
private readonly IJsonSerializer _jsonSerializer;
/// <summary>
/// Initializes a new instance of the <see cref="BlockListPropertyEditorBase"/> class.
/// </summary>
[Obsolete("Use non-obsoleted ctor. This will be removed in Umbraco 15.")]
protected BlockListPropertyEditorBase(IDataValueEditorFactory dataValueEditorFactory, IBlockValuePropertyIndexValueFactory blockValuePropertyIndexValueFactory)
: this(dataValueEditorFactory,blockValuePropertyIndexValueFactory, StaticServiceProvider.Instance.GetRequiredService<IJsonSerializer>())
{
}
/// <summary>
/// Initializes a new instance of the <see cref="BlockListPropertyEditorBase"/> class.
/// </summary>
protected BlockListPropertyEditorBase(IDataValueEditorFactory dataValueEditorFactory, IBlockValuePropertyIndexValueFactory blockValuePropertyIndexValueFactory, IJsonSerializer jsonSerializer)
: base(dataValueEditorFactory)
{
@@ -37,21 +44,27 @@ public abstract class BlockListPropertyEditorBase : DataEditor
SupportsReadOnly = true;
}
/// <inheritdoc/>
public override IPropertyIndexValueFactory PropertyIndexValueFactory => _blockValuePropertyIndexValueFactory;
#region Value Editor
/// <summary>
/// Instantiates a new <see cref="BlockEditorDataConverter"/> for use with the block list editor property value editor.
/// Instantiates a new <see cref="BlockEditorDataConverter{BlockListValue, BlockListLayoutItem}"/> for use with the block list editor property value editor.
/// </summary>
/// <returns>A new instance of <see cref="BlockListEditorDataConverter"/>.</returns>
protected virtual BlockEditorDataConverter<BlockListValue, BlockListLayoutItem> CreateBlockEditorDataConverter() => new BlockListEditorDataConverter(_jsonSerializer);
/// <inheritdoc/>
protected override IDataValueEditor CreateValueEditor() =>
DataValueEditorFactory.Create<BlockListEditorPropertyValueEditor>(Attribute!, CreateBlockEditorDataConverter());
/// <summary>
/// Defines the value editor for the block list property editors.
/// </summary>
internal class BlockListEditorPropertyValueEditor : BlockEditorPropertyValueEditor<BlockListValue, BlockListLayoutItem>
{
/// <summary>
/// Initializes a new instance of the <see cref="BlockListEditorPropertyValueEditor"/> class.
/// </summary>
public BlockListEditorPropertyValueEditor(
DataEditorAttribute attribute,
BlockEditorDataConverter<BlockListValue, BlockListLayoutItem> blockEditorDataConverter,
@@ -74,8 +87,15 @@ public abstract class BlockListPropertyEditorBase : DataEditor
Validators.Add(new MinMaxValidator(BlockEditorValues, textService));
}
/// <inheritdoc />
public override IValueRequiredValidator RequiredValidator => new BlockListValueRequiredValidator(JsonSerializer);
/// <inheritdoc/>
protected override BlockListValue CreateWithLayout(IEnumerable<BlockListLayoutItem> layout) => new(layout);
/// <summary>
/// Validates the min/max configuration for block list property editors.
/// </summary>
private class MinMaxValidator : BlockEditorMinMaxValidatorBase<BlockListValue, BlockListLayoutItem>
{
private readonly BlockEditorValues<BlockListValue, BlockListLayoutItem> _blockEditorValues;
@@ -100,12 +120,11 @@ public abstract class BlockListPropertyEditorBase : DataEditor
}
}
/// <inheritdoc/>
public override IEnumerable<Guid> ConfiguredElementTypeKeys()
{
var configuration = ConfigurationObject as BlockListConfiguration;
return configuration?.Blocks.SelectMany(ConfiguredElementTypeKeys) ?? Enumerable.Empty<Guid>();
}
}
#endregion
}
@@ -10,7 +10,6 @@ using Umbraco.Cms.Core.PropertyEditors.ValueConverters;
using Umbraco.Cms.Core.Serialization;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Strings;
using Umbraco.Extensions;
namespace Umbraco.Cms.Core.PropertyEditors;
@@ -1,11 +1,21 @@
// Copyright (c) Umbraco.
// See LICENSE for more details.
using System.ComponentModel.DataAnnotations;
using System.Text.Json.Nodes;
using Umbraco.Cms.Core.IO;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.Validation;
using Umbraco.Cms.Core.Serialization;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Strings;
using Umbraco.Extensions;
namespace Umbraco.Cms.Core.PropertyEditors;
/// <summary>
/// Represents a color picker property editor.
/// </summary>
[DataEditor(
Constants.PropertyEditors.Aliases.ColorPicker,
ValueEditorIsReusable = true)]
@@ -14,6 +24,9 @@ public class ColorPickerPropertyEditor : DataEditor
private readonly IIOHelper _ioHelper;
private readonly IConfigurationEditorJsonSerializer _configurationEditorJsonSerializer;
/// <summary>
/// Initializes a new instance of the <see cref="ColorPickerPropertyEditor"/> class.
/// </summary>
public ColorPickerPropertyEditor(IDataValueEditorFactory dataValueEditorFactory, IIOHelper ioHelper, IConfigurationEditorJsonSerializer configurationEditorJsonSerializer)
: base(dataValueEditorFactory)
{
@@ -22,10 +35,78 @@ public class ColorPickerPropertyEditor : DataEditor
SupportsReadOnly = true;
}
/// <inheritdoc/>
public override IPropertyIndexValueFactory PropertyIndexValueFactory { get; } = new NoopPropertyIndexValueFactory();
/// <inheritdoc />
protected override IDataValueEditor CreateValueEditor()
=> DataValueEditorFactory.Create<ColorPickerPropertyValueEditor>(Attribute!);
/// <inheritdoc />
protected override IConfigurationEditor CreateConfigurationEditor() =>
new ColorPickerConfigurationEditor(_ioHelper, _configurationEditorJsonSerializer);
/// <summary>
/// Defines the value editor for the color picker property editor.
/// </summary>
internal class ColorPickerPropertyValueEditor : DataValueEditor
{
/// <summary>
/// Initializes a new instance of the <see cref="ColorPickerPropertyValueEditor"/> class.
/// </summary>
public ColorPickerPropertyValueEditor(
IShortStringHelper shortStringHelper,
IJsonSerializer jsonSerializer,
IIOHelper ioHelper,
DataEditorAttribute attribute,
ILocalizedTextService localizedTextService)
: base(shortStringHelper, jsonSerializer, ioHelper, attribute)
{
Validators.AddRange(new ConfiguredColorValidator(localizedTextService));
}
/// <summary>
/// Validates the color selection for the color picker property editor.
/// </summary>
internal class ConfiguredColorValidator : IValueValidator
{
private readonly ILocalizedTextService _localizedTextService;
/// <summary>
/// Initializes a new instance of the <see cref="ConfiguredColorValidator"/> class.
/// </summary>
public ConfiguredColorValidator(ILocalizedTextService localizedTextService) => _localizedTextService = localizedTextService;
/// <inheritdoc/>
public IEnumerable<ValidationResult> Validate(object? value, string? valueType, object? dataTypeConfiguration, PropertyValidationContext validationContext)
{
if (value is null || value is not JsonObject valueAsJsonObject)
{
yield break;
}
if (dataTypeConfiguration is not ColorPickerConfiguration colorPickerConfiguration)
{
yield break;
}
string? selectedColor = valueAsJsonObject["value"]?.GetValue<string>();
if (selectedColor.IsNullOrWhiteSpace())
{
yield break;
}
IEnumerable<string> validColors = colorPickerConfiguration.Items.Select(x => EnsureConsistentColorRepresentation(x.Value));
if (validColors.Contains(EnsureConsistentColorRepresentation(selectedColor)) is false)
{
yield return new ValidationResult(
_localizedTextService.Localize("validation", "invalidColor", [selectedColor]),
["value"]);
}
}
private static string EnsureConsistentColorRepresentation(string color)
=> (color.StartsWith('#') ? color : $"#{color}").ToLowerInvariant();
}
}
}
@@ -1,29 +1,50 @@
// Copyright (c) Umbraco.
// See LICENSE for more details.
using Microsoft.Extensions.DependencyInjection;
using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.PropertyEditors.Validators;
using Umbraco.Cms.Core.Services;
namespace Umbraco.Cms.Core.PropertyEditors;
/// <summary>
/// Defines an email address property editor.
/// </summary>
[DataEditor(
Constants.PropertyEditors.Aliases.EmailAddress,
ValueEditorIsReusable = true)]
public class EmailAddressPropertyEditor : DataEditor
{
/// <summary>
/// The constructor will setup the property editor based on the attribute if one is found
/// </summary>
public EmailAddressPropertyEditor(IDataValueEditorFactory dataValueEditorFactory)
: base(dataValueEditorFactory)
=> SupportsReadOnly = true;
private readonly ILocalizedTextService _localizedTextService;
/// <summary>
/// Initializes a new instance of the <see cref="EmailAddressPropertyEditor"/> class.
/// </summary>
[Obsolete("Please use the constructor taking all parameters. Scheduled for removal in Umbraco 17.")]
public EmailAddressPropertyEditor(IDataValueEditorFactory dataValueEditorFactory)
: this(
dataValueEditorFactory,
StaticServiceProvider.Instance.GetRequiredService<ILocalizedTextService>())
{
}
/// <summary>
/// Initializes a new instance of the <see cref="EmailAddressPropertyEditor"/> class.
/// </summary>
public EmailAddressPropertyEditor(IDataValueEditorFactory dataValueEditorFactory, ILocalizedTextService localizedTextService)
: base(dataValueEditorFactory)
{
SupportsReadOnly = true;
_localizedTextService = localizedTextService;
}
/// <inheritdoc/>
protected override IDataValueEditor CreateValueEditor()
{
IDataValueEditor editor = base.CreateValueEditor();
// add an email address validator
editor.Validators.Add(new EmailValidator());
editor.Validators.Add(new EmailValidator(_localizedTextService));
return editor;
}
}
@@ -18,7 +18,7 @@ using Umbraco.Extensions;
namespace Umbraco.Cms.Core.PropertyEditors;
/// <summary>
/// Represents a media picker property editor.
/// Represents a media picker property editor.
/// </summary>
[DataEditor(
Constants.PropertyEditors.Aliases.MediaPicker3,
@@ -38,6 +38,7 @@ public class MediaPicker3PropertyEditor : DataEditor
SupportsReadOnly = true;
}
/// <inheritdoc />
public override IPropertyIndexValueFactory PropertyIndexValueFactory { get; } = new NoopPropertyIndexValueFactory();
/// <inheritdoc />
@@ -48,8 +49,9 @@ public class MediaPicker3PropertyEditor : DataEditor
protected override IDataValueEditor CreateValueEditor() =>
DataValueEditorFactory.Create<MediaPicker3PropertyValueEditor>(Attribute!);
/// <summary>
/// Defines the value editor for the media picker property editor.
/// </summary>
internal class MediaPicker3PropertyValueEditor : DataValueEditor, IDataValueReference
{
private readonly IDataTypeConfigurationCache _dataTypeReadCache;
@@ -60,6 +62,13 @@ public class MediaPicker3PropertyEditor : DataEditor
private readonly IScopeProvider _scopeProvider;
private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor;
/// <summary>
/// Initializes a new instance of the <see cref="MediaPicker3PropertyValueEditor"/> class.
/// </summary>
/// <remarks>
/// Note on FromEditor() and ToEditor() methods.
/// We do not want to transform the way the data is stored in the DB and would like to keep a raw JSON string.
/// </remarks>
public MediaPicker3PropertyValueEditor(
IShortStringHelper shortStringHelper,
IJsonSerializer jsonSerializer,
@@ -92,10 +101,7 @@ public class MediaPicker3PropertyEditor : DataEditor
Validators.Add(validators);
}
/// <remarks>
/// Note: no FromEditor() and ToEditor() methods
/// We do not want to transform the way the data is stored in the DB and would like to keep a raw JSON string
/// </remarks>
/// <inheritdoc/>
public IEnumerable<UmbracoEntityReference> GetReferences(object? value)
{
foreach (MediaWithCropsDto dto in Deserialize(_jsonSerializer, value))
@@ -104,6 +110,7 @@ public class MediaPicker3PropertyEditor : DataEditor
}
}
/// <inheritdoc/>
public override object ToEditor(IProperty property, string? culture = null, string? segment = null)
{
var value = property.GetValue(culture, segment);
@@ -111,7 +118,7 @@ public class MediaPicker3PropertyEditor : DataEditor
var dtos = Deserialize(_jsonSerializer, value).ToList();
dtos = UpdateMediaTypeAliases(dtos);
var configuration = _dataTypeReadCache.GetConfigurationAs<MediaPicker3Configuration>(property.PropertyType.DataTypeKey);
MediaPicker3Configuration? configuration = _dataTypeReadCache.GetConfigurationAs<MediaPicker3Configuration>(property.PropertyType.DataTypeKey);
if (configuration is not null)
{
foreach (MediaWithCropsDto dto in dtos)
@@ -123,6 +130,7 @@ public class MediaPicker3PropertyEditor : DataEditor
return dtos;
}
/// <inheritdoc/>
public override object? FromEditor(ContentPropertyData editorValue, object? currentValue)
{
if (editorValue.Value is null ||
@@ -145,6 +153,9 @@ public class MediaPicker3PropertyEditor : DataEditor
return _jsonSerializer.Serialize(mediaWithCropsDtos);
}
/// <summary>
/// Deserializes the provided JSON value into a list of <see cref="MediaWithCropsDto"/>.
/// </summary>
internal static IEnumerable<MediaWithCropsDto> Deserialize(IJsonSerializer jsonSerializer, object? value)
{
var rawJson = value is string str ? str : value?.ToString();
@@ -248,14 +259,29 @@ public class MediaPicker3PropertyEditor : DataEditor
/// </summary>
internal class MediaWithCropsDto
{
/// <summary>
/// Gets or sets the key.
/// </summary>
public Guid Key { get; set; }
/// <summary>
/// Gets or sets the media key.
/// </summary>
public Guid MediaKey { get; set; }
/// <summary>
/// Gets or sets the media type alias.
/// </summary>
public string MediaTypeAlias { get; set; } = string.Empty;
/// <summary>
/// Gets or sets the crops.
/// </summary>
public IEnumerable<ImageCropperValue.ImageCropperCrop>? Crops { get; set; }
/// <summary>
/// Gets or sets the focal point.
/// </summary>
public ImageCropperValue.ImageCropperFocalPoint? FocalPoint { get; set; }
/// <summary>
@@ -309,12 +335,19 @@ public class MediaPicker3PropertyEditor : DataEditor
}
}
/// <summary>
/// Validates the min/max configuration for the media picker property editor.
/// </summary>
internal class MinMaxValidator : ITypedJsonValidator<List<MediaWithCropsDto>, MediaPicker3Configuration>
{
private readonly ILocalizedTextService _localizedTextService;
/// <summary>
/// Initializes a new instance of the <see cref="MinMaxValidator"/> class.
/// </summary>
public MinMaxValidator(ILocalizedTextService localizedTextService) => _localizedTextService = localizedTextService;
/// <inheritdoc/>
public IEnumerable<ValidationResult> Validate(
List<MediaWithCropsDto>? mediaWithCropsDtos,
MediaPicker3Configuration? mediaPickerConfiguration,
@@ -363,12 +396,18 @@ public class MediaPicker3PropertyEditor : DataEditor
}
}
/// <summary>
/// Validates the allowed type configuration for the media picker property editor.
/// </summary>
internal class AllowedTypeValidator : ITypedJsonValidator<List<MediaWithCropsDto>, MediaPicker3Configuration>
{
private readonly ILocalizedTextService _localizedTextService;
private readonly IMediaTypeService _mediaTypeService;
private readonly IMediaService _mediaService;
/// <summary>
/// Initializes a new instance of the <see cref="AllowedTypeValidator"/> class.
/// </summary>
public AllowedTypeValidator(ILocalizedTextService localizedTextService, IMediaTypeService mediaTypeService, IMediaService mediaService)
{
_localizedTextService = localizedTextService;
@@ -376,6 +415,7 @@ public class MediaPicker3PropertyEditor : DataEditor
_mediaService = mediaService;
}
/// <inheritdoc/>
public IEnumerable<ValidationResult> Validate(
List<MediaWithCropsDto>? value,
MediaPicker3Configuration? configuration,
@@ -429,11 +469,17 @@ public class MediaPicker3PropertyEditor : DataEditor
}
}
/// <summary>
/// Validates the start node configuration for the media picker property editor.
/// </summary>
internal class StartNodeValidator : ITypedJsonValidator<List<MediaWithCropsDto>, MediaPicker3Configuration>
{
private readonly ILocalizedTextService _localizedTextService;
private readonly IMediaNavigationQueryService _mediaNavigationQueryService;
/// <summary>
/// Initializes a new instance of the <see cref="StartNodeValidator"/> class.
/// </summary>
public StartNodeValidator(
ILocalizedTextService localizedTextService,
IMediaNavigationQueryService mediaNavigationQueryService)
@@ -442,6 +488,7 @@ public class MediaPicker3PropertyEditor : DataEditor
_mediaNavigationQueryService = mediaNavigationQueryService;
}
/// <inheritdoc/>
public IEnumerable<ValidationResult> Validate(
List<MediaWithCropsDto>? value,
MediaPicker3Configuration? configuration,
@@ -453,53 +500,14 @@ public class MediaPicker3PropertyEditor : DataEditor
return [];
}
List<Guid> uniqueParentKeys = [];
foreach (Guid distinctMediaKey in value.DistinctBy(x => x.MediaKey).Select(x => x.MediaKey))
if (ValidationHelper.HasValidStartNode(value.Select(x => x.MediaKey), configuration.StartNodeId.Value, _mediaNavigationQueryService) is false)
{
if (_mediaNavigationQueryService.TryGetParentKey(distinctMediaKey, out Guid? parentKey) is false)
{
continue;
}
// If there is a start node, the media must have a parent.
if (parentKey is null)
{
return
[
new ValidationResult(
_localizedTextService.Localize("validation", "invalidStartNode"),
["value"])
];
}
uniqueParentKeys.Add(parentKey.Value);
}
IEnumerable<Guid> parentKeysNotInStartNode = uniqueParentKeys.Where(x => x != configuration.StartNodeId.Value);
foreach (Guid parentKey in parentKeysNotInStartNode)
{
if (_mediaNavigationQueryService.TryGetAncestorsKeys(parentKey, out IEnumerable<Guid> foundAncestorKeys) is false)
{
// We couldn't find the parent node, so we fail.
return
[
new ValidationResult(
_localizedTextService.Localize("validation", "invalidStartNode"),
["value"])
];
}
Guid[] ancestorKeys = foundAncestorKeys.ToArray();
if (ancestorKeys.Length == 0 || ancestorKeys.Contains(configuration.StartNodeId.Value) is false)
{
return
[
new ValidationResult(
_localizedTextService.Localize("validation", "invalidStartNode"),
["value"])
];
}
return
[
new ValidationResult(
_localizedTextService.Localize("validation", "invalidStartNode"),
["value"])
];
}
return [];
@@ -1,16 +1,27 @@
// Copyright (c) Umbraco.
// See LICENSE for more details.
using System.ComponentModel.DataAnnotations;
using System.Text.Json.Nodes;
using Microsoft.Extensions.DependencyInjection;
using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Core.IO;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.Editors;
using Umbraco.Cms.Core.Models.Entities;
using Umbraco.Cms.Core.Models.Validation;
using Umbraco.Cms.Core.PropertyEditors.Validation;
using Umbraco.Cms.Core.Scoping;
using Umbraco.Cms.Core.Serialization;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Strings;
using Umbraco.Extensions;
namespace Umbraco.Cms.Core.PropertyEditors;
/// <summary>
/// Represents a multi-node tree picker property editor.
/// </summary>
[DataEditor(
Constants.PropertyEditors.Aliases.MultiNodeTreePicker,
ValueType = ValueTypes.Text,
@@ -19,6 +30,9 @@ public class MultiNodeTreePickerPropertyEditor : DataEditor
{
private readonly IIOHelper _ioHelper;
/// <summary>
/// Initializes a new instance of the <see cref="MultiNodeTreePickerPropertyEditor"/> class.
/// </summary>
public MultiNodeTreePickerPropertyEditor(IDataValueEditorFactory dataValueEditorFactory, IIOHelper ioHelper)
: base(dataValueEditorFactory)
{
@@ -26,32 +40,75 @@ public class MultiNodeTreePickerPropertyEditor : DataEditor
SupportsReadOnly = true;
}
/// <inheritdoc/>
protected override IConfigurationEditor CreateConfigurationEditor() =>
new MultiNodePickerConfigurationEditor(_ioHelper);
/// <inheritdoc/>
protected override IDataValueEditor CreateValueEditor() =>
DataValueEditorFactory.Create<MultiNodeTreePickerPropertyValueEditor>(Attribute!);
/// <summary>
/// Defines the value editor for the media picker property editor.
/// </summary>
/// <remarks>
/// At first glance, the fromEditor and toEditor methods might seem strange.
/// This is because we wanted to stop the leaking of UDI's to the frontend while not having to do database migrations
/// so we opted to, for now, translate the udi string in the database into a structured format unique to the client
/// At first glance, the FromEditor and ToEditor methods might seem strange.
/// This is because we wanted to stop the leaking of UDIs to the frontend while not having to do database migrations
/// so we opted to, for now, translate the UDI string in the database into a structured format unique to the client.
/// This way, for now, no migration is needed and no changes outside of the editor logic needs to be touched to stop the leaking.
/// </remarks>
public class MultiNodeTreePickerPropertyValueEditor : DataValueEditor, IDataValueReference
{
private readonly IJsonSerializer _jsonSerializer;
/// <summary>
/// Initializes a new instance of the <see cref="MultiNodeTreePickerPropertyValueEditor"/> class.
/// </summary>
public MultiNodeTreePickerPropertyValueEditor(
IShortStringHelper shortStringHelper,
IJsonSerializer jsonSerializer,
IIOHelper ioHelper,
DataEditorAttribute attribute,
ILocalizedTextService localizedTextService,
IEntityService entityService,
ICoreScopeProvider coreScopeProvider,
IContentService contentService,
IMediaService mediaService,
IMemberService memberService)
: base(shortStringHelper, jsonSerializer, ioHelper, attribute)
{
_jsonSerializer = jsonSerializer;
Validators.Add(new TypedJsonValidatorRunner<EditorEntityReference[], MultiNodePickerConfiguration>(
jsonSerializer,
new MinMaxValidator(localizedTextService),
new ObjectTypeValidator(localizedTextService, coreScopeProvider, entityService),
new ContentTypeValidator(localizedTextService, coreScopeProvider, contentService, mediaService, memberService)));
}
/// <summary>
/// Initializes a new instance of the <see cref="MultiNodeTreePickerPropertyValueEditor"/> class.
/// </summary>
[Obsolete("Please use the constructor taking all parameters. Scheduled for removal in Umbraco 17.")]
public MultiNodeTreePickerPropertyValueEditor(
IShortStringHelper shortStringHelper,
IJsonSerializer jsonSerializer,
IIOHelper ioHelper,
DataEditorAttribute attribute)
: base(shortStringHelper, jsonSerializer, ioHelper, attribute)
: this(
shortStringHelper,
jsonSerializer,
ioHelper,
attribute,
StaticServiceProvider.Instance.GetRequiredService<ILocalizedTextService>(),
StaticServiceProvider.Instance.GetRequiredService<IEntityService>(),
StaticServiceProvider.Instance.GetRequiredService<ICoreScopeProvider>(),
StaticServiceProvider.Instance.GetRequiredService<IContentService>(),
StaticServiceProvider.Instance.GetRequiredService<IMediaService>(),
StaticServiceProvider.Instance.GetRequiredService<IMemberService>())
{
_jsonSerializer = jsonSerializer;
}
/// <inheritdoc/>
public IEnumerable<UmbracoEntityReference> GetReferences(object? value)
{
var asString = value == null ? string.Empty : value is string str ? str : value.ToString();
@@ -66,12 +123,13 @@ public class MultiNodeTreePickerPropertyEditor : DataEditor
}
}
/// <inheritdoc/>
public override object? FromEditor(ContentPropertyData editorValue, object? currentValue)
=> editorValue.Value is JsonArray jsonArray
? EntityReferencesToUdis(_jsonSerializer.Deserialize<IEnumerable<EditorEntityReference>>(jsonArray.ToJsonString()) ?? Enumerable.Empty<EditorEntityReference>())
: null;
/// <inheritdoc/>
public override object? ToEditor(IProperty property, string? culture = null, string? segment = null)
{
var value = property.GetValue(culture, segment);
@@ -80,7 +138,7 @@ public class MultiNodeTreePickerPropertyEditor : DataEditor
: null;
}
private IEnumerable<EditorEntityReference> UdisToEntityReferences(IEnumerable<string> stringUdis)
private static IEnumerable<EditorEntityReference> UdisToEntityReferences(IEnumerable<string> stringUdis)
{
foreach (var stringUdi in stringUdis)
{
@@ -93,14 +151,277 @@ public class MultiNodeTreePickerPropertyEditor : DataEditor
}
}
private string EntityReferencesToUdis(IEnumerable<EditorEntityReference> nodeReferences)
private static string EntityReferencesToUdis(IEnumerable<EditorEntityReference> nodeReferences)
=> string.Join(",", nodeReferences.Select(entityReference => Udi.Create(entityReference.Type, entityReference.Unique).ToString()));
/// <summary>
/// Describes and editor entity reference.
/// </summary>
public class EditorEntityReference
{
/// <summary>
/// Gets or sets the entity object type.
/// </summary>
public required string Type { get; set; }
/// <summary>
/// Gets or sets the entity unique identifier.
/// </summary>
public required Guid Unique { get; set; }
}
/// <summary>
/// Gets the name of the configured object type for documents.
/// </summary>
internal const string DocumentObjectType = "content";
/// <summary>
/// Gets the name of the configured object type for media.
/// </summary>
internal const string MediaObjectType = "media";
/// <summary>
/// Gets the name of the configured object type for members.
/// </summary>
internal const string MemberObjectType = "member";
/// <inheritdoc/>
/// <summary>
/// Validates the min/max configuration for the multi-node tree picker property editor.
/// </summary>
internal class MinMaxValidator : ITypedJsonValidator<EditorEntityReference[], MultiNodePickerConfiguration>
{
private readonly ILocalizedTextService _localizedTextService;
/// <summary>
/// Initializes a new instance of the <see cref="MinMaxValidator"/> class.
/// </summary>
public MinMaxValidator(ILocalizedTextService localizedTextService) => _localizedTextService = localizedTextService;
/// <inheritdoc/>
public IEnumerable<ValidationResult> Validate(
EditorEntityReference[]? entityReferences,
MultiNodePickerConfiguration? configuration,
string? valueType,
PropertyValidationContext validationContext)
{
var validationResults = new List<ValidationResult>();
if (configuration is null)
{
return validationResults;
}
if (configuration.MinNumber > 0 && (entityReferences is null || entityReferences.Length < configuration.MinNumber))
{
validationResults.Add(new ValidationResult(
_localizedTextService.Localize(
"validation",
"entriesShort",
[configuration.MinNumber.ToString(), (configuration.MinNumber - (entityReferences?.Length ?? 0)).ToString()
]),
["value"]));
}
if (entityReferences is null)
{
return validationResults;
}
if (configuration.MaxNumber > 0 && entityReferences.Length > configuration.MaxNumber)
{
validationResults.Add(new ValidationResult(
_localizedTextService.Localize(
"validation",
"entriesExceed",
[configuration.MaxNumber.ToString(), (entityReferences.Length - configuration.MaxNumber).ToString()
]),
["value"]));
}
return validationResults;
}
}
/// <inheritdoc/>
/// <summary>
/// Validates the selected object type for the multi-node tree picker property editor.
/// </summary>
internal class ObjectTypeValidator : ITypedJsonValidator<EditorEntityReference[], MultiNodePickerConfiguration>
{
private readonly ILocalizedTextService _localizedTextService;
private readonly ICoreScopeProvider _coreScopeProvider;
private readonly IEntityService _entityService;
/// <summary>
/// Initializes a new instance of the <see cref="ObjectTypeValidator"/> class.
/// </summary>
public ObjectTypeValidator(
ILocalizedTextService localizedTextService,
ICoreScopeProvider coreScopeProvider,
IEntityService entityService)
{
_localizedTextService = localizedTextService;
_coreScopeProvider = coreScopeProvider;
_entityService = entityService;
}
/// <inheritdoc/>
public IEnumerable<ValidationResult> Validate(
EditorEntityReference[]? entityReferences,
MultiNodePickerConfiguration? configuration,
string? valueType,
PropertyValidationContext validationContext)
{
var validationResults = new List<ValidationResult>();
if (entityReferences is null || configuration?.TreeSource?.ObjectType is null)
{
return validationResults;
}
Guid[] uniqueKeys = entityReferences.DistinctBy(x => x.Unique).Select(x => x.Unique).ToArray();
if (uniqueKeys.Length == 0)
{
return validationResults;
}
Guid? allowedObjectType = GetObjectType(configuration.TreeSource.ObjectType);
if (allowedObjectType is null)
{
return
[
// Some invalid object type was sent.
new ValidationResult(
_localizedTextService.Localize(
"validation",
"invalidObjectType"),
["value"])
];
}
using ICoreScope scope = _coreScopeProvider.CreateCoreScope();
foreach (Guid key in uniqueKeys)
{
IEntitySlim? entity = _entityService.Get(key);
if (entity is not null && entity.NodeObjectType != allowedObjectType)
{
validationResults.Add(new ValidationResult(
_localizedTextService.Localize(
"validation",
"invalidObjectType"),
["value"]));
}
}
scope.Complete();
return validationResults;
}
private static Guid? GetObjectType(string objectType) =>
objectType switch
{
DocumentObjectType => Constants.ObjectTypes.Document,
MediaObjectType => Constants.ObjectTypes.Media,
MemberObjectType => Constants.ObjectTypes.Member,
_ => null,
};
}
/// <inheritdoc/>
/// <summary>
/// Validates the selected content type for the multi-node tree picker property editor.
/// </summary>
internal class ContentTypeValidator : ITypedJsonValidator<EditorEntityReference[], MultiNodePickerConfiguration>
{
private readonly ILocalizedTextService _localizedTextService;
private readonly ICoreScopeProvider _coreScopeProvider;
private readonly IContentService _contentService;
private readonly IMediaService _mediaService;
private readonly IMemberService _memberService;
/// <summary>
/// Initializes a new instance of the <see cref="ContentTypeValidator"/> class.
/// </summary>
public ContentTypeValidator(
ILocalizedTextService localizedTextService,
ICoreScopeProvider coreScopeProvider,
IContentService contentService,
IMediaService mediaService,
IMemberService memberService)
{
_localizedTextService = localizedTextService;
_coreScopeProvider = coreScopeProvider;
_contentService = contentService;
_mediaService = mediaService;
_memberService = memberService;
}
/// <inheritdoc/>
public IEnumerable<ValidationResult> Validate(
EditorEntityReference[]? entityReferences,
MultiNodePickerConfiguration? configuration,
string? valueType,
PropertyValidationContext validationContext)
{
var validationResults = new List<ValidationResult>();
Guid[] allowedTypes = configuration?.Filter?.Split(Constants.CharArrays.Comma, StringSplitOptions.RemoveEmptyEntries).Select(Guid.Parse).ToArray() ?? [];
// We can't validate if there is no object type, and we don't need to if there's no filter.
if (entityReferences is null || allowedTypes.Length == 0 || configuration?.TreeSource?.ObjectType is null)
{
return validationResults;
}
using ICoreScope scope = _coreScopeProvider.CreateCoreScope();
Guid?[] uniqueContentTypeKeys = entityReferences
.Select(x => x.Unique)
.Distinct()
.Select(x => GetContent(configuration.TreeSource.ObjectType, x))
.Select(x => x?.ContentType.Key)
.Distinct()
.ToArray();
scope.Complete();
foreach (Guid? key in uniqueContentTypeKeys)
{
if (key is null)
{
validationResults.Add(new ValidationResult(
_localizedTextService.Localize(
"validation",
"missingContent"),
["value"]));
continue;
}
if (allowedTypes.Contains(key.Value) is false)
{
validationResults.Add(new ValidationResult(
_localizedTextService.Localize(
"validation",
"invalidObjectType"),
["value"]));
}
}
return validationResults;
}
private IContentBase? GetContent(string objectType, Guid key) =>
objectType switch
{
DocumentObjectType => _contentService.GetById(key),
MediaObjectType => _mediaService.GetById(key),
MemberObjectType => _memberService.GetById(key),
_ => null,
};
}
}
}
@@ -1,6 +1,7 @@
// Copyright (c) Umbraco.
// See LICENSE for more details.
using System.ComponentModel.DataAnnotations;
using System.Runtime.Serialization;
using Microsoft.Extensions.Logging;
using Umbraco.Cms.Core.IO;
@@ -8,10 +9,13 @@ using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.ContentEditing;
using Umbraco.Cms.Core.Models.Editors;
using Umbraco.Cms.Core.Models.PublishedContent;
using Umbraco.Cms.Core.Models.Validation;
using Umbraco.Cms.Core.PropertyEditors.Validation;
using Umbraco.Cms.Core.Routing;
using Umbraco.Cms.Core.Serialization;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Strings;
using Umbraco.Extensions;
namespace Umbraco.Cms.Core.PropertyEditors;
@@ -41,6 +45,9 @@ public class MultiUrlPickerValueEditor : DataValueEditor, IDataValueReference
_jsonSerializer = jsonSerializer;
_contentService = contentService;
_mediaService = mediaService;
Validators.Add(new TypedJsonValidatorRunner<LinkDisplay[], MultiUrlPickerConfiguration>(
_jsonSerializer,
new MinMaxValidator(localizedTextService)));
}
public IEnumerable<UmbracoEntityReference> GetReferences(object? value)
@@ -209,4 +216,51 @@ public class MultiUrlPickerValueEditor : DataValueEditor, IDataValueReference
[DataMember(Name = "queryString")]
public string? QueryString { get; set; }
}
internal class MinMaxValidator : ITypedJsonValidator<LinkDisplay[], MultiUrlPickerConfiguration>
{
private readonly ILocalizedTextService _localizedTextService;
public MinMaxValidator(ILocalizedTextService localizedTextService) => _localizedTextService = localizedTextService;
public IEnumerable<ValidationResult> Validate(
LinkDisplay[]? linksDtos,
MultiUrlPickerConfiguration? multiUrlPickerConfiguration,
string? valueType,
PropertyValidationContext validationContext)
{
if (multiUrlPickerConfiguration is null || (linksDtos is null && multiUrlPickerConfiguration.MinNumber == 0))
{
return [];
}
if (linksDtos is null || linksDtos.Length < multiUrlPickerConfiguration.MinNumber)
{
return [new ValidationResult(
_localizedTextService.Localize(
"validation",
"entriesShort",
[multiUrlPickerConfiguration.MinNumber.ToString(), (multiUrlPickerConfiguration.MinNumber - (linksDtos?.Length ?? 0)).ToString()]),
["value"])];
}
if (linksDtos.Length > multiUrlPickerConfiguration.MaxNumber && multiUrlPickerConfiguration.MaxNumber > 0)
{
return
[
new ValidationResult(
_localizedTextService.Localize(
"validation",
"entriesExceed",
[
multiUrlPickerConfiguration.MaxNumber.ToString(),
(linksDtos.Length - multiUrlPickerConfiguration.MaxNumber).ToString()
]),
["value"])
];
}
return [];
}
}
}
@@ -6,14 +6,17 @@ using Umbraco.Cms.Core.Exceptions;
using Umbraco.Cms.Core.IO;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.Editors;
using Umbraco.Cms.Core.Models.Validation;
using Umbraco.Cms.Core.PropertyEditors.Validators;
using Umbraco.Cms.Core.Serialization;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Strings;
using Umbraco.Extensions;
namespace Umbraco.Cms.Core.PropertyEditors;
/// <summary>
/// Represents a multiple text string property editor.
/// Represents a multiple text string property editor.
/// </summary>
[DataEditor(
Constants.PropertyEditors.Aliases.MultipleTextstring,
@@ -24,7 +27,7 @@ public class MultipleTextStringPropertyEditor : DataEditor
private readonly IIOHelper _ioHelper;
/// <summary>
/// Initializes a new instance of the <see cref="MultipleTextStringPropertyEditor" /> class.
/// Initializes a new instance of the <see cref="MultipleTextStringPropertyEditor" /> class.
/// </summary>
public MultipleTextStringPropertyEditor(IIOHelper ioHelper, IDataValueEditorFactory dataValueEditorFactory)
: base(dataValueEditorFactory)
@@ -42,37 +45,42 @@ public class MultipleTextStringPropertyEditor : DataEditor
new MultipleTextStringConfigurationEditor(_ioHelper);
/// <summary>
/// Custom value editor so we can format the value for the editor and the database
/// Defines the value editor for the multiple text string property editor.
/// </summary>
internal class MultipleTextStringPropertyValueEditor : DataValueEditor
{
private static readonly string NewLine = "\n";
private static readonly string[] NewLineDelimiters = { "\r\n", "\r", "\n" };
private static readonly string _newLine = "\n";
private static readonly string[] _newLineDelimiters = { "\r\n", "\r", "\n" };
/// <summary>
/// Initializes a new instance of the <see cref="MultipleTextStringPropertyValueEditor"/> class.
/// </summary>
public MultipleTextStringPropertyValueEditor(
IShortStringHelper shortStringHelper,
IJsonSerializer jsonSerializer,
IIOHelper ioHelper,
DataEditorAttribute attribute)
DataEditorAttribute attribute,
ILocalizedTextService localizedTextService)
: base(shortStringHelper, jsonSerializer, ioHelper, attribute)
{
Validators.AddRange(new MinMaxValidator(localizedTextService));
}
/// <summary>
/// A custom FormatValidator is used as for multiple text strings, each string should individually be checked
/// against the configured regular expression, rather than the JSON representing all the strings as a whole.
/// A custom <see href="IValueFormatValidator" /> is used as for multiple text strings, each string should individually
/// be checked against the configured regular expression, rather than the JSON representing all the strings as a whole.
/// </summary>
public override IValueFormatValidator FormatValidator => new MultipleTextStringFormatValidator();
/// <summary>
/// The value passed in from the editor will be an array of simple objects so we'll need to parse them to get the
/// string
/// The value passed in from the editor will be an array of simple objects so we'll need to parse them to get the
/// string.
/// </summary>
/// <param name="editorValue"></param>
/// <param name="currentValue"></param>
/// <returns></returns>
/// <remarks>
/// We will also check the pre-values here, if there are more items than what is allowed we'll just trim the end
/// We will also check the pre-values here, if there are more items than what is allowed we'll just trim the end.
/// </remarks>
public override object? FromEditor(ContentPropertyData editorValue, object? currentValue)
{
@@ -93,30 +101,35 @@ public class MultipleTextStringPropertyEditor : DataEditor
// only allow the max if over 0
if (max > 0)
{
return string.Join(NewLine, value.Take(max));
return string.Join(_newLine, value.Take(max));
}
return string.Join(NewLine, value);
return string.Join(_newLine, value);
}
/// <inheritdoc/>
public override object ToEditor(IProperty property, string? culture = null, string? segment = null)
{
var value = property.GetValue(culture, segment);
// The legacy property editor saved this data as new line delimited! strange but we have to maintain that.
return value is string stringValue
? stringValue.Split(NewLineDelimiters, StringSplitOptions.None)
? stringValue.Split(_newLineDelimiters, StringSplitOptions.None)
: Array.Empty<string>();
}
}
/// <summary>
/// A custom <see href="IValueFormatValidator" /> to check each string against the configured format.
/// </summary>
internal class MultipleTextStringFormatValidator : IValueFormatValidator
{
/// <inheritdoc/>
public IEnumerable<ValidationResult> ValidateFormat(object? value, string valueType, string format)
{
if (value is not IEnumerable<string> textStrings)
{
return Enumerable.Empty<ValidationResult>();
return [];
}
var textStringValidator = new RegexValidator();
@@ -129,7 +142,60 @@ public class MultipleTextStringPropertyEditor : DataEditor
}
}
return Enumerable.Empty<ValidationResult>();
return [];
}
}
/// <summary>
/// Validates the min/max configuration for the multiple text strings property editor.
/// </summary>
internal class MinMaxValidator : IValueValidator
{
private readonly ILocalizedTextService _localizedTextService;
/// <summary>
/// Initializes a new instance of the <see cref="MinMaxValidator"/> class.
/// </summary>
public MinMaxValidator(ILocalizedTextService localizedTextService) => _localizedTextService = localizedTextService;
/// <inheritdoc/>
public IEnumerable<ValidationResult> Validate(object? value, string? valueType, object? dataTypeConfiguration, PropertyValidationContext validationContext)
{
if (dataTypeConfiguration is not MultipleTextStringConfiguration multipleTextStringConfiguration)
{
yield break;
}
// If we have a null value, treat as an empty collection for minimum number validation.
if (value is not IEnumerable<string> stringValues)
{
stringValues = [];
}
var stringCount = stringValues.Count();
if (stringCount < multipleTextStringConfiguration.Min)
{
if (stringCount == 1)
{
yield return new ValidationResult(
_localizedTextService.Localize("validation", "outOfRangeSingleItemMinimum", [multipleTextStringConfiguration.Min.ToString()]),
["value"]);
}
else
{
yield return new ValidationResult(
_localizedTextService.Localize("validation", "outOfRangeMultipleItemsMinimum", [stringCount.ToString(), multipleTextStringConfiguration.Min.ToString()]),
["value"]);
}
}
if (multipleTextStringConfiguration.Max > 0 && stringCount > multipleTextStringConfiguration.Max)
{
yield return new ValidationResult(
_localizedTextService.Localize("validation", "outOfRangeMultipleItemsMaximum", [stringCount.ToString(), multipleTextStringConfiguration.Max.ToString()]),
["value"]);
}
}
}
}
@@ -11,33 +11,34 @@ using Umbraco.Cms.Core.Strings;
namespace Umbraco.Cms.Core.PropertyEditors;
/// <summary>
/// A value editor to handle posted json array data and to return array data for the multiple selected csv items
/// A value editor to handle posted JSON array data and to return array data for the multiple selected CSV items.
/// </summary>
/// <remarks>
/// This is re-used by editors such as the multiple drop down list or check box list
/// This is re-used by editors such as the multiple drop down list or check box list.
/// </remarks>
public class MultipleValueEditor : DataValueEditor
{
private readonly IJsonSerializer _jsonSerializer;
/// <summary>
/// Initializes a new instance of the <see cref="MultipleValueEditor"/> class.
/// </summary>
public MultipleValueEditor(
ILocalizedTextService localizedTextService,
IShortStringHelper shortStringHelper,
IJsonSerializer jsonSerializer,
IIOHelper ioHelper,
DataEditorAttribute attribute)
: base(localizedTextService, shortStringHelper, jsonSerializer, ioHelper, attribute)
: base(shortStringHelper, jsonSerializer, ioHelper, attribute)
{
_jsonSerializer = jsonSerializer;
Validators.Add(new MultipleValueValidator());
Validators.Add(new MultipleValueValidator(localizedTextService));
}
/// <summary>
/// When multiple values are selected a json array will be posted back so we need to format for storage in
/// the database which is a comma separated string value
/// When multiple values are selected a JSON array will be posted back so we need to format for storage in
/// the database which is a comma separated string value.
/// </summary>
/// <param name="editorValue"></param>
/// <param name="currentValue"></param>
/// <returns></returns>
public override object? FromEditor(ContentPropertyData editorValue, object? currentValue)
{
@@ -46,7 +47,6 @@ public class MultipleValueEditor : DataValueEditor
return null;
}
var result = _jsonSerializer.Serialize(stringValues);
return result;
return _jsonSerializer.Serialize(stringValues);
}
}
@@ -1,9 +1,14 @@
// Copyright (c) Umbraco.
// See LICENSE for more details.
using System.ComponentModel.DataAnnotations;
using Umbraco.Cms.Core.IO;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.Validation;
using Umbraco.Cms.Core.Serialization;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Strings;
using Umbraco.Extensions;
namespace Umbraco.Cms.Core.PropertyEditors;
@@ -20,7 +25,7 @@ public class RadioButtonsPropertyEditor : DataEditor
private readonly IConfigurationEditorJsonSerializer _configurationEditorJsonSerializer;
/// <summary>
/// The constructor will setup the property editor based on the attribute if one is found
/// Initializes a new instance of the <see cref="RadioButtonsPropertyEditor"/> class.
/// </summary>
public RadioButtonsPropertyEditor(IDataValueEditorFactory dataValueEditorFactory, IIOHelper ioHelper, IConfigurationEditorJsonSerializer configurationEditorJsonSerializer)
: base(dataValueEditorFactory)
@@ -30,13 +35,71 @@ public class RadioButtonsPropertyEditor : DataEditor
SupportsReadOnly = true;
}
/// <summary>
/// Return a custom pre-value editor
/// </summary>
/// <returns></returns>
/// <inheritdoc/>
protected override IConfigurationEditor CreateConfigurationEditor() =>
new ValueListConfigurationEditor(_ioHelper, _configurationEditorJsonSerializer);
protected override IDataValueEditor CreateValueEditor() =>
DataValueEditorFactory.Create<RadioValueEditor>(Attribute!);
/// <inheritdoc/>
protected override IDataValueEditor CreateValueEditor()
=> DataValueEditorFactory.Create<RadioButtonsPropertyValueEditor>(Attribute!);
/// <summary>
/// Defines the value editor for the radio buttons property editor.
/// </summary>
internal class RadioButtonsPropertyValueEditor : DataValueEditor
{
/// <summary>
/// Initializes a new instance of the <see cref="RadioButtonsPropertyValueEditor"/> class.
/// </summary>
public RadioButtonsPropertyValueEditor(
IShortStringHelper shortStringHelper,
IJsonSerializer jsonSerializer,
IIOHelper ioHelper,
DataEditorAttribute attribute,
ILocalizedTextService localizedTextService)
: base(shortStringHelper, jsonSerializer, ioHelper, attribute)
=> Validators.AddRange([new RadioButtonValueValidator(localizedTextService)]);
}
/// <summary>
/// Validates the prevalue configuration for the radio buttons property editor.
/// </summary>
internal class RadioButtonValueValidator : IValueValidator
{
private readonly ILocalizedTextService _localizedTextService;
/// <summary>
/// Initializes a new instance of the <see cref="RadioButtonValueValidator"/> class.
/// </summary>
/// <param name="localizedTextService"></param>
public RadioButtonValueValidator(ILocalizedTextService localizedTextService) => _localizedTextService = localizedTextService;
/// <inheritdoc/>
public IEnumerable<ValidationResult> Validate(object? value, string? valueType, object? dataTypeConfiguration, PropertyValidationContext validationContext)
{
if (value == null || value.ToString().IsNullOrWhiteSpace())
{
yield break;
}
if (dataTypeConfiguration is not ValueListConfiguration valueListConfiguration)
{
yield break;
}
if (value is not string valueAsString)
{
yield break;
}
if (valueListConfiguration.Items.Contains(valueAsString) is false)
{
yield return new ValidationResult(
_localizedTextService.Localize("validation", "notOneOfOptions", [valueAsString]),
["value"]);
}
}
}
}
@@ -6,11 +6,12 @@ using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.Editors;
using Umbraco.Cms.Core.Serialization;
using Umbraco.Cms.Core.Strings;
using Umbraco.Cms.Infrastructure.PropertyEditors.Validators;
namespace Umbraco.Cms.Core.PropertyEditors;
/// <summary>
/// Represents a checkbox property and parameter editor.
/// Represents a true/false (toggle) property editor.
/// </summary>
[DataEditor(
Constants.PropertyEditors.Aliases.Boolean,
@@ -29,8 +30,14 @@ public class TrueFalsePropertyEditor : DataEditor
protected override IDataValueEditor CreateValueEditor()
=> DataValueEditorFactory.Create<TrueFalsePropertyValueEditor>(Attribute!);
/// <summary>
/// Defines the value editor for the true/false (toggle) property editor.
/// </summary>
internal class TrueFalsePropertyValueEditor : DataValueEditor
{
/// <summary>
/// Initializes a new instance of the <see cref="TrueFalsePropertyValueEditor"/> class.
/// </summary>
public TrueFalsePropertyValueEditor(
IShortStringHelper shortStringHelper,
IJsonSerializer jsonSerializer,
@@ -40,10 +47,17 @@ public class TrueFalsePropertyEditor : DataEditor
{
}
/// <inheritdoc />
public override IValueRequiredValidator RequiredValidator => new TrueFalseValueRequiredValidator();
/// <inheritdoc/>
public override object? ToEditor(IProperty property, string? culture = null, string? segment = null)
=> ParsePropertyValue(property.GetValue(culture, segment));
// NOTE: property editor value type is Integer, which means we need to store the boolean representation as 0 or 1
/// <inheritdoc/>
/// <remarks>
/// NOTE: property editor value type is Integer, which means we need to store the boolean representation as 0 or 1.
/// </remarks>
public override object? FromEditor(ContentPropertyData editorValue, object? currentValue)
=> ParsePropertyValue(editorValue.Value) ? 1 : 0;
@@ -0,0 +1,40 @@
using System.ComponentModel.DataAnnotations;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Models.Blocks;
using Umbraco.Cms.Core.PropertyEditors.Validators;
using Umbraco.Cms.Core.Serialization;
namespace Umbraco.Cms.Infrastructure.PropertyEditors.Validators;
/// <summary>
/// Custom validator for block value required validation.
/// </summary>
internal class BlockListValueRequiredValidator : RequiredValidator
{
private readonly IJsonSerializer _jsonSerializer;
/// <summary>
/// Initializes a new instance of the <see cref="BlockListValueRequiredValidator"/> class.
/// </summary>
public BlockListValueRequiredValidator(IJsonSerializer jsonSerializer) => _jsonSerializer = jsonSerializer;
/// <inheritdoc/>
public override IEnumerable<ValidationResult> ValidateRequired(object? value, string? valueType)
{
IEnumerable<ValidationResult> validationResults = base.ValidateRequired(value, valueType);
if (value is null)
{
return validationResults;
}
if (_jsonSerializer.TryDeserialize(value, out BlockListValue? blockListValue) &&
blockListValue.ContentData.Count == 0 &&
blockListValue.Layout.Count == 0)
{
validationResults = validationResults.Append(new ValidationResult(Constants.Validation.ErrorMessages.Properties.Empty, ["value"]));
}
return validationResults;
}
}
@@ -0,0 +1,29 @@
using System.ComponentModel.DataAnnotations;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.PropertyEditors.Validators;
namespace Umbraco.Cms.Infrastructure.PropertyEditors.Validators;
/// <summary>
/// Custom validator for true/false (toggle) required validation.
/// </summary>
internal class TrueFalseValueRequiredValidator : RequiredValidator
{
/// <inheritdoc/>
public override IEnumerable<ValidationResult> ValidateRequired(object? value, string? valueType)
{
IEnumerable<ValidationResult> validationResults = base.ValidateRequired(value, valueType);
if (value is null)
{
return validationResults;
}
if (value is bool valueAsBool && valueAsBool is false)
{
validationResults = validationResults.Append(new ValidationResult(Constants.Validation.ErrorMessages.Properties.Empty, ["value"]));
}
return validationResults;
}
}
@@ -1,8 +1,6 @@
using Microsoft.Extensions.Options;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.PublishedContent;
using Umbraco.Cms.Core.Services;
using Umbraco.Extensions;
namespace Umbraco.Cms.Infrastructure.PublishedCache;
@@ -22,33 +20,9 @@ internal class ReservedFieldNamesService : IReservedFieldNamesService
_mediaPropertySettings = mediaPropertySettings.Value;
}
public ISet<string> GetDocumentReservedFieldNames()
{
var reservedProperties = typeof(IPublishedContent).GetPublicProperties().Select(x => x.Name).ToHashSet();
var reservedMethods = typeof(IPublishedContent).GetPublicMethods().Select(x => x.Name).ToHashSet();
reservedProperties.UnionWith(reservedMethods);
reservedProperties.UnionWith(_contentPropertySettings.ReservedFieldNames);
public ISet<string> GetDocumentReservedFieldNames() => _contentPropertySettings.ReservedFieldNames;
return reservedProperties;
}
public ISet<string> GetMediaReservedFieldNames() => _mediaPropertySettings.ReservedFieldNames;
public ISet<string> GetMediaReservedFieldNames()
{
var reservedProperties = typeof(IPublishedContent).GetPublicProperties().Select(x => x.Name).ToHashSet();
var reservedMethods = typeof(IPublishedContent).GetPublicMethods().Select(x => x.Name).ToHashSet();
reservedProperties.UnionWith(reservedMethods);
reservedProperties.UnionWith(_mediaPropertySettings.ReservedFieldNames);
return reservedProperties;
}
public ISet<string> GetMemberReservedFieldNames()
{
var reservedProperties = typeof(IPublishedMember).GetPublicProperties().Select(x => x.Name).ToHashSet();
var reservedMethods = typeof(IPublishedMember).GetPublicMethods().Select(x => x.Name).ToHashSet();
reservedProperties.UnionWith(reservedMethods);
reservedProperties.UnionWith(_memberPropertySettings.ReservedFieldNames);
return reservedProperties;
}
public ISet<string> GetMemberReservedFieldNames() => _memberPropertySettings.ReservedFieldNames;
}
@@ -18,6 +18,7 @@ public class UmbracoTreeSearcherFields : IUmbracoTreeSearcherFields
UmbracoExamineFieldNames.CategoryFieldName,
"parentID",
UmbracoExamineFieldNames.ItemTypeFieldName,
UmbracoExamineFieldNames.IndexPathFieldName,
};
private readonly ISet<string> _backOfficeMediaFieldsToLoad =
@@ -0,0 +1,37 @@
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Persistence.Querying;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Infrastructure.Persistence;
namespace Umbraco.Cms.Infrastructure.Services;
internal sealed class ContentTypeSearchService : IContentTypeSearchService
{
private readonly ISqlContext _sqlContext;
private readonly IContentTypeService _contentTypeService;
public ContentTypeSearchService(ISqlContext sqlContext, IContentTypeService contentTypeService)
{
_sqlContext = sqlContext;
_contentTypeService = contentTypeService;
}
public async Task<PagedModel<IContentType>> SearchAsync(string query, bool? isElement, CancellationToken cancellationToken, int skip = 0, int take = 100)
{
// if the query is a GUID, search for that explicitly
Guid.TryParse(query, out Guid guidQuery);
IQuery<IContentType> nameQuery = isElement is not null ?
_sqlContext.Query<IContentType>().Where(x => (x.Name!.Contains(query) || x.Key == guidQuery) && x.IsElement == isElement) :
_sqlContext.Query<IContentType>().Where(x => x.Name!.Contains(query) || x.Key == guidQuery);
IContentType[] contentTypes = (await _contentTypeService.GetByQueryAsync(nameQuery, cancellationToken)).ToArray();
return new PagedModel<IContentType>
{
Items = contentTypes.Skip(skip).Take(take),
Total = contentTypes.Count()
};
}
}
@@ -52,7 +52,7 @@ internal sealed class IndexedEntitySearchService : IIndexedEntitySearchService
int skip = 0,
int take = 100,
bool ignoreUserStartNodes = false)
=> Search(objectType, query, parentId, null, skip, take, ignoreUserStartNodes);
=> Search(objectType, query, parentId, null, null, skip, take, ignoreUserStartNodes);
public PagedModel<IEntitySlim> Search(
UmbracoObjectTypes objectType,
@@ -62,6 +62,17 @@ internal sealed class IndexedEntitySearchService : IIndexedEntitySearchService
int skip = 0,
int take = 100,
bool ignoreUserStartNodes = false)
=> Search(objectType, query, parentId, contentTypeIds, null, skip, take, ignoreUserStartNodes);
public PagedModel<IEntitySlim> Search(
UmbracoObjectTypes objectType,
string query,
Guid? parentId,
IEnumerable<Guid>? contentTypeIds,
bool? trashed,
int skip = 0,
int take = 100,
bool ignoreUserStartNodes = false)
{
UmbracoEntityTypes entityType = objectType switch
{
@@ -91,6 +102,7 @@ internal sealed class IndexedEntitySearchService : IIndexedEntitySearchService
pageNumber,
out var totalFound,
contentTypeAliases,
trashed,
ignoreUserStartNodes: ignoreUserStartNodes,
searchFrom: parentId?.ToString());
@@ -353,7 +353,7 @@ public class PackagingService : IPackagingService
}
// Set additional values
installedPackage.AllowPackageTelemetry = packageManifest.AllowTelemetry;
installedPackage.AllowPackageTelemetry = packageManifest is { AllowTelemetry: true, AllowPackageTelemetry: true };
if (!string.IsNullOrEmpty(packageManifest.Version))
{
@@ -1,4 +1,4 @@
using Examine;
using Examine;
using Microsoft.Extensions.Logging;
using Umbraco.Cms.Core.Cache;
using Umbraco.Cms.Infrastructure.Examine;
@@ -7,7 +7,8 @@ namespace Umbraco.Cms.Infrastructure.Services;
public class IndexingRebuilderService : IIndexingRebuilderService
{
private const string TempKey = "temp_indexing_op_";
private const string IsRebuildingIndexRuntimeCacheKeyPrefix = "temp_indexing_op_";
private readonly IAppPolicyCache _runtimeCache;
private readonly IIndexRebuilder _indexRebuilder;
private readonly ILogger<IndexingRebuilderService> _logger;
@@ -49,7 +50,7 @@ public class IndexingRebuilderService : IIndexingRebuilderService
private void Set(string indexName)
{
var cacheKey = TempKey + indexName;
var cacheKey = IsRebuildingIndexRuntimeCacheKeyPrefix + indexName;
// put temp val in cache which is used as a rudimentary way to know when the indexing is done
_runtimeCache.Insert(cacheKey, () => "tempValue", TimeSpan.FromMinutes(5));
@@ -57,13 +58,13 @@ public class IndexingRebuilderService : IIndexingRebuilderService
private void Clear(string? indexName)
{
var cacheKey = TempKey + indexName;
var cacheKey = IsRebuildingIndexRuntimeCacheKeyPrefix + indexName;
_runtimeCache.Clear(cacheKey);
}
public bool IsRebuilding(string indexName)
{
var cacheKey = "temp_indexing_op_" + indexName;
var cacheKey = IsRebuildingIndexRuntimeCacheKeyPrefix + indexName;
return _runtimeCache.Get(cacheKey) is not null;
}
@@ -1,30 +1,45 @@
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Umbraco.Cms.Core.Cache;
using Umbraco.Cms.Core.Configuration.Models;
using Umbraco.Cms.Core.Logging;
using Umbraco.Cms.Core.PublishedCache;
using Umbraco.Cms.Core.Scoping;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Infrastructure.HostedServices;
using Umbraco.Cms.Infrastructure.HybridCache.Persistence;
namespace Umbraco.Cms.Infrastructure.HybridCache;
/// <summary>
/// Rebuilds the published content cache in the database.
/// </summary>
internal class DatabaseCacheRebuilder : IDatabaseCacheRebuilder
{
private const string NuCacheSerializerKey = "Umbraco.Web.PublishedCache.NuCache.Serializer";
private const string IsRebuildingDatabaseCacheRuntimeCacheKey = "temp_database_cache_rebuild_op";
private readonly IDatabaseCacheRepository _databaseCacheRepository;
private readonly ICoreScopeProvider _coreScopeProvider;
private readonly IOptions<NuCacheSettings> _nucacheSettings;
private readonly IKeyValueService _keyValueService;
private readonly ILogger<DatabaseCacheRebuilder> _logger;
private readonly IProfilingLogger _profilingLogger;
private readonly IBackgroundTaskQueue _backgroundTaskQueue;
private readonly IAppPolicyCache _runtimeCache;
/// <summary>
/// Initializes a new instance of the <see cref="DatabaseCacheRebuilder"/> class.
/// </summary>
public DatabaseCacheRebuilder(
IDatabaseCacheRepository databaseCacheRepository,
ICoreScopeProvider coreScopeProvider,
IOptions<NuCacheSettings> nucacheSettings,
IKeyValueService keyValueService,
ILogger<DatabaseCacheRebuilder> logger, IProfilingLogger profilingLogger)
ILogger<DatabaseCacheRebuilder> logger,
IProfilingLogger profilingLogger,
IBackgroundTaskQueue backgroundTaskQueue,
IAppPolicyCache runtimeCache)
{
_databaseCacheRepository = databaseCacheRepository;
_coreScopeProvider = coreScopeProvider;
@@ -32,18 +47,63 @@ internal class DatabaseCacheRebuilder : IDatabaseCacheRebuilder
_keyValueService = keyValueService;
_logger = logger;
_profilingLogger = profilingLogger;
_backgroundTaskQueue = backgroundTaskQueue;
_runtimeCache = runtimeCache;
}
public void Rebuild()
/// <inheritdoc/>
public bool IsRebuilding() => _runtimeCache.Get(IsRebuildingDatabaseCacheRuntimeCacheKey) is not null;
/// <inheritdoc/>
public void Rebuild() => Rebuild(false);
/// <inheritdoc/>
public void Rebuild(bool useBackgroundThread)
{
using ICoreScope scope = _coreScopeProvider.CreateCoreScope();
_databaseCacheRepository.Rebuild();
scope.Complete();
if (useBackgroundThread)
{
_logger.LogInformation("Starting async background thread for rebuilding database cache.");
_backgroundTaskQueue.QueueBackgroundWorkItem(
cancellationToken =>
{
using (ExecutionContext.SuppressFlow())
{
Task.Run(() => PerformRebuild());
return Task.CompletedTask;
}
});
}
else
{
PerformRebuild();
}
}
private void PerformRebuild()
{
try
{
SetIsRebuilding();
using ICoreScope scope = _coreScopeProvider.CreateCoreScope();
_databaseCacheRepository.Rebuild();
scope.Complete();
}
finally
{
ClearIsRebuilding();
}
}
private void SetIsRebuilding() => _runtimeCache.Insert(IsRebuildingDatabaseCacheRuntimeCacheKey, () => "tempValue", TimeSpan.FromMinutes(10));
private void ClearIsRebuilding() => _runtimeCache.Clear(IsRebuildingDatabaseCacheRuntimeCacheKey);
/// <inheritdoc/>
public void RebuildDatabaseCacheIfSerializerChanged()
{
using var scope = _coreScopeProvider.CreateCoreScope();
using ICoreScope scope = _coreScopeProvider.CreateCoreScope();
NuCacheSerializerType serializer = _nucacheSettings.Value.NuCacheSerializerType;
var currentSerializerValue = _keyValueService.GetValue(NuCacheSerializerKey);
@@ -54,11 +114,12 @@ internal class DatabaseCacheRebuilder : IDatabaseCacheRebuilder
_logger.LogWarning(
"Database cache was serialized using {CurrentSerializer}. Currently configured cache serializer {Serializer}. Rebuilding database cache.",
currentSerializer, serializer);
currentSerializer,
serializer);
using (_profilingLogger.TraceDuration<DatabaseCacheRebuilder>($"Rebuilding database cache with {serializer} serializer"))
{
Rebuild();
Rebuild(false);
_keyValueService.SetValue(NuCacheSerializerKey, serializer.ToString());
}
@@ -291,8 +291,8 @@ internal class MediaCacheService : IMediaCacheService
private HybridCacheEntryOptions GetSeedEntryOptions() => new()
{
Expiration = _cacheSettings.SeedCacheDuration,
LocalCacheExpiration = _cacheSettings.SeedCacheDuration,
Expiration = _cacheSettings.Entry.Media.SeedCacheDuration,
LocalCacheExpiration = _cacheSettings.Entry.Media.SeedCacheDuration,
};
private string GetCacheKey(Guid key, bool preview) => preview ? $"{key}+draft" : $"{key}";
@@ -68,7 +68,13 @@ public static class HttpContextExtensions
// Otherwise we can't log in as both a member and a backoffice user
// For instance if you've enabled basic auth.
ClaimsPrincipal? authenticatedPrincipal = result.Principal;
IEnumerable<ClaimsIdentity> existingIdentities = httpContext.User.Identities.Where(x => x.IsAuthenticated && x.AuthenticationType != authenticatedPrincipal.Identity.AuthenticationType);
// Make sure to copy into a list before attempting to update the authenticated principal, so we don't attempt to modify
// the collection while iterating it.
// See: https://github.com/umbraco/Umbraco-CMS/issues/18509
var existingIdentities = httpContext.User.Identities
.Where(x => x.IsAuthenticated && x.AuthenticationType != authenticatedPrincipal.Identity.AuthenticationType)
.ToList();
authenticatedPrincipal.AddIdentities(existingIdentities);
httpContext.User = authenticatedPrincipal;

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