Compare commits

...
Author SHA1 Message Date
Sven Geusens 31f0ad4490 Revert "remove dotnet docfx global flag"
This reverts commit b6307fb359.
2026-01-13 16:56:59 +01:00
Sven Geusens b6307fb359 remove dotnet docfx global flag 2026-01-13 16:26:15 +01:00
Sven Geusens d37b5a2edb Use newer version of docfx by installing it as a dotnet tool instead of trough chocolatey 2026-01-13 14:22:00 +01:00
CopilotGitHubcopilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>nielslyngsoeNiels Lyngsø
dc459bca67 Add loading indicator to composition picker modal (#21086)
* Initial plan

* Add loading indicator to composition picker modal

Co-authored-by: nielslyngsoe <6791648+nielslyngsoe@users.noreply.github.com>

* Document implementation and verify changes

Co-authored-by: nielslyngsoe <6791648+nielslyngsoe@users.noreply.github.com>

* Address code review feedback - add accessibility and documentation

Co-authored-by: nielslyngsoe <6791648+nielslyngsoe@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: nielslyngsoe <6791648+nielslyngsoe@users.noreply.github.com>
Co-authored-by: Niels Lyngsø <nsl@umbraco.dk>
2026-01-13 13:14:40 +00:00
CopilotGitHubcopilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>nielslyngsoeMads Rasmussen
2a7478e7f4 Add loading indicator to data type picker flow modal (#21085)
* Initial plan

* Add loading indicator to data-type-picker-flow-modal

- Added _isLoading state variable
- Updated #getDataTypes() to set loading state with try-finally
- Added uui-loader component in #renderGrid() when loading
- Created test file with basic tests
- Fixed linter warnings

Co-authored-by: nielslyngsoe <6791648+nielslyngsoe@users.noreply.github.com>

* Refactor: Replace inline styles with CSS class for loader

- Added .loader-container CSS class
- Removed inline styles from loader div
- Improves maintainability and follows best practices

Co-authored-by: nielslyngsoe <6791648+nielslyngsoe@users.noreply.github.com>

* Improve tests and revert unrelated package-lock.json changes

- Test observable behavior (loader element) instead of private properties
- Added tests for loader visibility during loading states
- Added test for loader removal after loading completes
- Reverted unintended changes to Umbraco.Web.UI.Login/package-lock.json

Co-authored-by: nielslyngsoe <6791648+nielslyngsoe@users.noreply.github.com>

* Refactor: Extract helper function in tests for cleaner code

- Added setLoadingState helper function to reduce code duplication
- Updated comments to reflect testing reactive state property
- Improved test readability and maintainability

Co-authored-by: nielslyngsoe <6791648+nielslyngsoe@users.noreply.github.com>

* delete test file not testing anything

* show loading indicator in search field instead

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: nielslyngsoe <6791648+nielslyngsoe@users.noreply.github.com>
Co-authored-by: Mads Rasmussen <madsr@hey.com>
2026-01-13 13:05:59 +00:00
Niels LyngsøandGitHub 7b5a087ea5 Sidebar: Make scale grab-area smaller (#21228)
make split view scale area smaller
2026-01-13 13:57:54 +01:00
Andy ButlandandGitHub fefb9ca7aa Docs: Add branch naming convention details to CLAUDE.md (#21311)
* Update Claude memory files to provide further instruction on branch naming convention.

* Resolved points raised in code review.
2026-01-13 13:50:15 +01:00
Andy ButlandandGitHub 8f584e1a49 Tests: Fix intermittent failure in RecycleBinMediaProtectionHelperTests (#21332)
Use thread-safe collections in unit tests verifying functionality that uses parallel processing.
2026-01-13 10:20:35 +01:00
de02456cf8 E2E: QA Added acceptance tests for removing not-found items (#21371)
* Added tests for removing a not-found member picker

* Added tests for adding thumbnail to block

* Updated tests to match the test helper changes

* Refactor code to avoid duplication

* Added tests for removing a thumbnail from a block list/grid and refactor code

* Bumped version

* Bumped version and make tests run in the pipeline

* Update smokeTest command to use '@smoke' filter

---------

Co-authored-by: Andreas Zerbst <73799582+andr317c@users.noreply.github.com>
2026-01-13 03:57:04 +00:00
f47a456515 Datatype Collection: Add fallback icon for datatype (#21269)
Add fallback icon for datatype

Co-authored-by: Engiber Lozada <89547469+engijlr@users.noreply.github.com>
2026-01-12 15:22:41 +01:00
engjlr 234907aedb Merge branch 'main' of https://github.com/umbraco/Umbraco-CMS 2026-01-12 13:57:57 +01:00
CopilotJacob Overgaardcopilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>nielslyngsoeNiels LyngsøEngiber Lozadaengjlr
e3ff3ea3b9 Add loader and error handling to MFA modal (#21089)
* Initial plan

* Add loader and error handling to MFA modal

Co-authored-by: nielslyngsoe <6791648+nielslyngsoe@users.noreply.github.com>

* Use localized error messages in MFA modal

Co-authored-by: nielslyngsoe <6791648+nielslyngsoe@users.noreply.github.com>

* Refactor render logic into separate #renderContent method

Co-authored-by: nielslyngsoe <6791648+nielslyngsoe@users.noreply.github.com>

* Revert src/Umbraco.Web.UI.Login/package-lock.json to main branch version

* Removed unused variable.

* Center the loader in MFA modal.

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: nielslyngsoe <6791648+nielslyngsoe@users.noreply.github.com>
Co-authored-by: Niels Lyngsø <nsl@umbraco.dk>
Co-authored-by: Engiber Lozada <89547469+engijlr@users.noreply.github.com>
Co-authored-by: engjlr <enl@umbraco.dk>
2026-01-12 13:51:59 +01:00
Jacob OvergaardandClaude Sonnet 4.5 6050bdd40f Tree pickers: Implement noAccess property UI handling for user start nodes
- Add noAccess observable to document and media tree item contexts
- Add visual styling (grayed out, italic, cursor: not-allowed) for noAccess items in tree views
- Implement click prevention to block navigation when noAccess is true
- Update document and media picker input contexts with type-safe guards to prevent selection of noAccess items
- Create type guard utilities (isDocumentTreeItem, isMediaTreeItem) in separate utils files
- Items with noAccess are shown for navigation but cannot be selected or opened

This implements Task 63363 for Feature 63060 "Handle Start Nodes"

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-12 10:52:10 +01:00
8c23cb2ffd Block Grid: Resolve translation keys for group names (closes #20696) (#21362)
* Block Grid: Resolve translation keys for group names (closes #20696)

Add localization support for block group names in two locations:
- Block Grid area type permission combobox options
- Block catalogue modal group headers

Translation keys (e.g., #content_isPublished) used as group names
are now properly resolved instead of displaying the raw key.

* fix: moves group.name to mapper so search works

* fix: localizes block types in permissions element too

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-12 08:16:08 +00:00
faba205025 Collections: fix create action causing full page navigation (#21366)
Fix collection create action causing full page navigation instead of SPA routing

When a collection create action had an href, clicking it would trigger a full
browser navigation instead of using history.pushState for SPA routing. This was
caused by event.stopPropagation() being called before the early return when an
href was present, preventing the global ensureAnchorHistory() listener from
intercepting the click event.

The fix moves stopPropagation() to only execute when we're actually handling
the click via execute() (no href), allowing href-based navigation to bubble
to the window-level router listener for proper SPA navigation.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-12 07:55:55 +01:00
Chris HoustonandGitHub 35c940364c Code Quality: Fix SA1500, SA1111 and SA1134 StyleCop warnings (#21369)
style: fix SA1500, SA1111, SA1134 StyleCop warnings

Fixed 194 StyleCop analyzer warnings across the codebase:

- SA1500: Move opening braces to their own line for multi-line statements (80 warnings)
- SA1111: Move closing parenthesis to same line as last parameter (50 warnings)
- SA1134: Place each attribute on its own line (64 warnings)

Also added XML documentation to any public APIs within the edited files.
2026-01-12 07:06:56 +01:00
Andy ButlandandGitHub f4ff1da043 Cache: Resolve thread safety issues in RepositoryCacheKeys and FullDataSetRepositoryCachePolicy (closes #21350) (#21355)
* Verify and resolve thread safety issues in RepositoryCacheKeys.

* Verify and resolve thread safety issues in FullDataSetRepositoryCachePolicy.
2026-01-12 06:58:13 +01:00
Niels Lyngsø 30aea87e28 correct comments 2026-01-09 21:33:01 +01:00
Niels LyngsøandGitHub 93229fd457 Mobile navigation: enable horizontal scroll on small screens (#21354)
* make backoffice minimum 800px wide

* minimum 920px
2026-01-09 14:19:47 +00:00
4f29bcd8d6 Fix #20769, added support for clip-text for UmbTableColumn (#20808)
Co-authored-by: Markus Johansson <markus@obviuse.se>
Co-authored-by: Mads Rasmussen <madsr@hey.com>
2026-01-09 13:26:42 +00:00
Niels LyngsøandGitHub 80dddc430c declare events in the global event interface map (#21349) 2026-01-09 13:12:41 +01:00
5a33a55f21 Block workspace: Enforce "AllowEditInvariantFromNonDefault" in variant blocks (closes #20633) (#20868)
* implement preventEditInvariantFromNonDefault for variant blocks

* remove unused

* refactor to enforce both document and block case from the document module

* remove implementation from doc workspace

* prevent editing invariant blocks from non default

* remove unused imports

* more explicit class names

* Refactor invariant edit guard rule creation

Extracted the creation of the invariant property edit guard rule into a reusable _createRule method in the controller base class. Updated block and document workspace controllers to use this method

* Refactor invariant edit rule logic into base controller

Moved the logic for observing properties and variant options and applying property guard rules into a new _observeAndApplyRule method in the base controller. Updated document block and workspace controllers to use this shared method, reducing code duplication and improving maintainability.

* Refactor invariant block edit check into helper methods

Extracted logic for checking invariant blocks and default language datasets into private async methods for better readability and maintainability. This refactor also corrects a context check typo and improves error handling.

* remove unused

---------

Co-authored-by: Niels Lyngsø <nsl@umbraco.dk>
2026-01-09 09:51:05 +00:00
Mads RasmussenandGitHub 4276e903d7 Fix MSW typescript compile errors (#21319)
Refactor mock handlers to use typed request/response models
2026-01-09 08:50:16 +01:00
Andy ButlandandGitHub c5098f6df3 Multi-node picker: Validate content type when filter is configured but object type is not (closes #21338) (#21342)
Validate content type for multi-node picker when content types are defined but the object type is not (consider as being for a document).
2026-01-09 06:48:42 +01:00
aa0d636503 Entity Signs: rounded infobox top corners (#21341)
* correct styling for entity sign infobox top corners

* Used shorthand notation for padding.

---------

Co-authored-by: engjlr <enl@umbraco.dk>
2026-01-08 17:47:14 +00:00
aka James4uandGitHub a0e1908d2b fix(slider): enforce Maximum Value configuration (closes #21323) (#21339)
- Fixed inverted logic in #parseNumber method
- Changed Number.isFinite(num) ? undefined : num to Number.isFinite(num) ? num : undefined
- Previously, valid max values (e.g., 50) were being ignored and defaulting to 100
- Now correctly parses and uses the configured Maximum Value from data type settings

This ensures the Maximum Value setting in Slider datatype configuration
is properly enforced in the slider UI component.
2026-01-08 17:49:12 +01:00
3e57bb8128 Multi-link picker: Allow drag and drop into empty link picker (closes #21295) (#21325)
* fix(backoffice): allow drag and drop into empty link picker (closes #21295)

* refactor: use classMap to avoid empty class attribute

* refactor: use margin/padding trick for drop zone

* refactor: use CSS :has() selector instead of class

* also enable it for the Document input

---------

Co-authored-by: Niels Lyngsø <niels.lyngso@gmail.com>
2026-01-08 15:24:42 +01:00
Dirk SeefeldandGitHub 0094c19780 Gitignore: Exclude acceptance test results from git (#21336)
* exclude acceptance test results from git

* reorder .gitignore
2026-01-08 13:01:03 +00:00
Andy ButlandandGitHub ed162feddf Server Events: Route document updated event when public access entries are modified (closes #21237) (#21310)
* Route server events for public access updates to indicate an update to the protected document.

* Add unit tests for all ServerEventSender notification handlers.

* Adds additional test recommended in code review.

* Addressed parameter name issue raised in code review.
2026-01-08 09:31:27 +01:00
Andreas ZerbstandGitHub 28e9607910 QA: E2E skip running sqlite acceptance test on nightly pipeline (#21331)
* Added skip condition for SQLite

* Added missing comma

* Added missing bracket

* Followed naming convention of parameters and added comments
2026-01-08 08:32:02 +01:00
Andy Butland b8d02067fe Merge branch 'release/17.1'
# Conflicts:
#	src/Umbraco.Web.UI.Client/package.json
#	version.json
2026-01-08 06:36:50 +01:00
3fd311bce8 E2E: QA added temporary waits to flaky acceptance (#21324)
* Added wait

* Additional fixes

* More fixes

---------

Co-authored-by: Nhu Dinh <150406148+nhudinh0309@users.noreply.github.com>
2026-01-08 03:13:54 +00:00
calmandGitHub 55b77157ec Backoffice entity actions: prevent entity actions dropdown from closing on first click (#21322)
fix(backoffice): prevent entity actions dropdown from closing on first open (closes #21320)
2026-01-07 17:04:12 +01:00
906cbb50d8 Removed margin-top to address the loader icon shifting when entering … (#21301)
* Removed margin-top to address the loader icon shifting when entering text

* Space

---------

Co-authored-by: Engiber Lozada <89547469+engijlr@users.noreply.github.com>
2026-01-07 13:39:08 +00:00
Engiber LozadaandGitHub 93dff1a7a3 Entity Sings: Add localization keys to core entity signs. (#21302)
Improving localization support for core entity signs.
2026-01-07 14:26:31 +01:00
Niels Lyngsø 962db17628 lint fixes 2026-01-07 13:36:57 +01:00
4df4ee9c31 Fix login validation messages not showing on submit (#21306)
Co-authored-by: Andreas Zerbst <73799582+andr317c@users.noreply.github.com>
2026-01-07 13:34:57 +01:00
Andreas ZerbstandGitHub c37fab8e4e E2E: QA Update acceptance to use new assertion pattern (#21297)
* Updated tests

* Updated culture

* Updated tests to use new assertion helpers

* Updated helpers

* Updated tests

* Updated tests

* Updated tests

* Bumped version

* Bumped version
2026-01-07 12:19:02 +00:00
bfc8053c11 Docker: Fix Docker template healthcheck, bind mounts, and HTTPS support (fixes #21278) (#21299)
* Fix test entrypoint

* Fix healthcheck.sh

* Ensure bind mounts gets created on host

* Fix bind mounts permission issues

* Generate self signed cert for dev

* Only generate cert for localhost

* Fixup docker compose file

* Update templates/UmbracoProject/entrypoint.sh

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

* Update templates/UmbracoProject/Dockerfile

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

* Fix APP_UID runtime availability and optimize chown performance

- Export APP_UID as ENV so it's available at container runtime
  (ARG values from .NET base image are only available at build time)
- Only run chown -R when directory ownership differs from APP_UID
  to avoid slow recursive operations on large directories

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-07 12:56:58 +01:00
50b2c8e03b Performance: Only flush ID/Key map in ContentCacheRefresher on content deletion (#21283)
* Clean-up of ContentCacheRefresher: resolved warnings and tidied up code and comments.

* Only flush the ID/Key map when content is deleted.

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

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

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-01-07 12:14:36 +01:00
Andy Butland 2564d04941 Merge branch 'main' of https://github.com/umbraco/Umbraco-CMS 2026-01-07 11:40:47 +01:00
Andy Butland ca3091e92e Fixed version of Microsoft.Extensions.Caching.Hybrid from incorrect one added in earlier commit. 2026-01-07 11:40:33 +01:00
Niels Lyngsø 036225a76d early return if manager is not present 2026-01-07 11:39:49 +01:00
307f3be505 User group: add description to user group (discussions/14986) (#21057)
* Adding description to user groups

* Add description to dto and test and umbraco plan

* update unit test for user group

* remove change from link picker

* edit icon ui for user group

* Fixed table exists check in migration.

* update description in table user groups

* update user group editor css

* update description default

* remove description column element

* add ignore large method

* remove codesence

* update user group descriptions default

* Added description to constructor of ReadOnlyUserGroup.

---------

Co-authored-by: Lan Nguyen Thuy <lnt@umbraco.dk>
Co-authored-by: Andy Butland <abutland73@gmail.com>
Co-authored-by: Andreas Zerbst <73799582+andr317c@users.noreply.github.com>
2026-01-07 11:17:04 +01:00
Niels LyngsøandNiels Lyngsø b5fb17682c Hotfix: refactor of #21221 (#21293)
* refactor reload method to entity-detail

* refactor property value transfering when variation changes
2026-01-07 10:53:11 +01:00
Andy ButlandandNiels Lyngsø 83882a94bd Bumped version to 17.1.0.
# Conflicts:
#	src/Umbraco.Web.UI.Client/package.json
#	version.json
2026-01-07 10:53:11 +01:00
0c5156c7b3 Content: Fix property variation change breaking document save via Infinite Editing (closes #21195) (#21221)
When changing a property's variation setting (Shared/Invariant ↔ Variant) via Infinite Editing,
the document would fail to save with a 404 error.

This fix:
- Adds value migration fallback logic in UmbPropertyValuePresetVariantBuilderController
  to find values when culture/segment doesn't match exactly
- Overrides reload() in UmbContentDetailWorkspaceContextBase to process incoming data
  through _processIncomingData() for proper value transformation
- Detects property variation changes and triggers document reload to migrate values

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-07 10:52:52 +01:00
Kenn JacobsenandGitHub c859b2883d Members: Fix IMemberService.GetByKeysAsync() (#21312)
Fix IMemberService.GetByKeysAsync()
2026-01-07 07:54:20 +00:00
2aa49a3004 Performance: Avoid database lookup in UserIdKeyResolver for super-user (#21281)
* Avoid an unnecessary look-up for the super-user when resolving ID from key and vice versa.
Utilise an async database methods given the enclosing method is async.

* Applied suggestions from code review.

* Revered async amend (caused pipeline failures with integration tests).

* Apply suggestions from code review

Co-authored-by: Nikolaj Geisle <70372949+Zeegaan@users.noreply.github.com>

---------

Co-authored-by: Nikolaj Geisle <70372949+Zeegaan@users.noreply.github.com>
2026-01-07 08:37:59 +01:00
Andy ButlandandGitHub 54a1364a88 Dependencies: Update Microsoft packages to 10.0.1 and pin vulnerable transitive dependencies (closes #21122) (#21285)
* Added transitive dependency references to specific libraries where direct dependencies depend on vulnerable versions.

* Enable CentralPackageTransitivePinningEnabled.

* Update MS dependencies to 10.0.1 patch versions.

* Removed System.Text.Encodings.Web.

* Add TODOs for pinned dependency removal.
2026-01-07 06:58:12 +00:00
603788a039 Extension insights: Hide icon from table collection (#21268)
Hide icon from table collection

Co-authored-by: Engiber Lozada <89547469+engijlr@users.noreply.github.com>
2026-01-06 15:00:24 +00:00
dependabot[bot]andJacob Overgaard c8454684d0 Bump qs
Bumps the npm_and_yarn group with 1 update in the /src/Umbraco.Web.UI.Client directory: [qs](https://github.com/ljharb/qs).


Updates `qs` from 6.14.0 to 6.14.1
- [Changelog](https://github.com/ljharb/qs/blob/main/CHANGELOG.md)
- [Commits](https://github.com/ljharb/qs/compare/v6.14.0...v6.14.1)

---
updated-dependencies:
- dependency-name: qs
  dependency-version: 6.14.1
  dependency-type: indirect
  dependency-group: npm_and_yarn
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-01-06 15:46:22 +01:00
848df27482 Auto close focus leave (#20700)
* working on a auto closing ... modal when focus leaves

* remove appsetting for local development

* rewrites the focus function to check if the shadow dom exsits before trying to set focus

* Fix JSON formatting in appsettings.Development.template.json

* Simplify focus logic in entity action list

Refactored the focus method to directly focus the first menu item after it is rendered, removing unnecessary nested updateComplete checks and focusing logic for the label button.

* Remove unused 'nothing' import and minor formatting

* Call ext.component?.focus() instead of chaining updateComplete promises.

* Update entity-action-list.element.ts

---------

Co-authored-by: Niels Lyngsø <nsl@umbraco.dk>
Co-authored-by: Engiber Lozada <89547469+engijlr@users.noreply.github.com>
Co-authored-by: engjlr <enl@umbraco.dk>
2026-01-06 13:43:25 +00:00
6ed4075eeb Added localize label for entity sign bundle (#21252)
* Added localize for entity sign bundle label

* Updated how entity-sign-bundle.element handles label localize

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

* Removed unnecessary conditional check for localize entity-sign-bundle

* Moved localize into render method for entity-sign-bundle.element

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Engiber Lozada <89547469+engijlr@users.noreply.github.com>
2026-01-06 14:00:48 +01:00
0da1fa116e Markdown Conversion: Remove hard dependency on deprecated library and replace with IMarkdownToHtmlConverter abstraction (closes #21238 and #19500) (#21242)
* Add IMarkdownToHtmlConverter abstraction with Markdig and HeyRed implementations

- Add IMarkdownToHtmlConverter interface in Umbraco.Core.Strings
- Add MarkdigMarkdownToHtmlConverter using the Markdig library (new default)
- Add HeyRedMarkdownToHtmlConverter using HeyRed.MarkdownSharp (deprecated, for backwards compatibility)
- Update HealthChecks.MarkdownToHtmlConverter to use the new abstraction
- Update MarkdownEditorValueConverter to use the new abstraction
- Add unit tests for both markdown converter implementations
- Add unit tests for HealthChecks.MarkdownToHtmlConverter syntax highlighting

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* Apply suggestions from code review

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

* Apply suggestions from code review

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

* Apply suggestion from code review.

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-01-06 08:43:47 +01:00
Nhu DinhandGitHub 51cec0e41f E2E: QA Updated acceptance tests for max length validation message (#21230)
* Updated max length validation message

* Fixed format
2026-01-05 17:26:14 +07:00
b563561d2a Management API: Scope notification headers and add document inclusion abstraction (closes #21231, #21241 and #21240) (#21244)
* Adds a check to ensure notifications are only added to relevant CMS management API endpoints.

* Add null check for tagging actions to handle null management API GroupName.

* Provide abstraction for the DocInclusionPredicate when configuring SwaggerGenOptions.

* Apply suggestions from code review

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

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-01-05 11:16:41 +01:00
c003751466 Content Type Cache: Clear ContentTypeCommonRepository cache when data types change (closes #21261) (#21289)
Cache: Clear ContentTypeCommonRepository cache when data types change

When a data type's EditorAlias is changed (e.g., BlockList to SingleBlock), the ContentTypeCommonRepository 5-minute cache was not being cleared. This caused content types to return stale PropertyEditorAlias values until server restart, leading to validation using the wrong property editor validators.

The fix adds IContentTypeCommonRepository as a dependency to DataTypeCacheRefresher and calls ClearCache() in RefreshInternal(), matching the pattern already used in ContentTypeCacheRefresher and TemplateCacheRefresher.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-05 18:02:36 +09:00
caa01b5122 Code quality: resolve build warnings CS0169 and CS0649 (#21258)
* Fixed the 22 warnings.

Also updated the XML documentation where required.

* Updating XML docs.

* Noted version in obsoletion message.

---------

Co-authored-by: Andy Butland <abutland73@gmail.com>
2026-01-04 11:52:25 +00:00
Chris HoustonandGitHub b14a7ca3e3 Code Quality: Fix ASP0019 warnings by replacing Headers.Add with Headers.Append (#21260)
Use Append for response headers

- Replaces Add with Append when adding response headers to conform to IHeaderDictionary usage.
- Applies across unauthorized handling, redirects, and custom header signaling.
- Updates tests to use Append consistently.
- Removes ASP0019 from warnings in project configs to reflect new approach.
2025-12-31 12:00:15 +01:00
Chris HoustonandGitHub 0803ab8bda Build Warnings: Suppress ASPDEPR003 warnings in DevelopmentMode.Backoffice (#21259)
Silences deprecation warning in dev mode

Silences deprecated runtime-compile warning in dev mode
Keeps development-time runtime compilation enabled for in-memory dev setup
Relates to ASPDEPR003
2025-12-31 11:29:55 +01:00
Nhu DinhandGitHub ac86d97c4e E2E: QA Update acceptance tests to use refactored UI helpers (#21266)
* Bumped version

* Updated tests to match new changes

* Bumped version

* Bumped version

* Use isSuccessNotificationVisible instead of doesSuccessNotificationHaveText

* Updated tests to avoid flaky tests

* Bumped version of test helper

* Bumped version of test helper

* Bumped version

* Bumped version

* Bumped version

* Update waits to avoid hardcoded values
2025-12-31 14:47:53 +07:00
cbde6af9c0 Code quality: Fixing XML docs issues CS1570, CS1572, CS1723, & CS1575 (#21256)
* FIxing XML docs issues CS1570, CS1572, CS1723, & CS1575

* Update ContentImagingSettings.cs

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

* Update ITypedSingleBlockListProcessor.cs

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

* Update PublishedContentExtensions.cs

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

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-12-26 10:57:16 +01:00
Chris HoustonandGitHub da8a532080 Code quality: Remove obsolete serialization constructors (fixing SYSLIB0051 warnings) (#21235)
* fix(SYSLIB0051): Remove obsolete serialization constructors

Removes obsolete formatter-based serialization constructors from exception
classes to resolve SYSLIB0051 warnings. This is NOT a breaking change as:

- Binary serialization is deprecated in modern .NET
- No BinaryFormatter usage exists in the codebase
- Microsoft recommends removing these obsolete constructors

Affected files:
- AuthorizationException.cs
- BootFailedException.cs
- ConfigurationException.cs
- PanicException.cs
- UnattendedInstallException.cs
- RetryLimitExceededException.cs
- IncompleteMigrationExpressionException.cs
- HttpUmbracoFormRouteStringException.cs
- ModelBindingException.cs

Also removes SYSLIB0051 from WarningsNotAsErrors in project files.

* fix(SYSLIB0051): Mark obsolete serialization constructors for removal in v19
2025-12-24 15:46:29 +01:00
cf7623e5bd Code quality: Resolve CS0108 compiler warnings by adding explicit 'new' keyword (#21236)
* fix: Resolve CS0108 compiler warnings by adding explicit 'new' keyword

fix: Resolve CS0108 compiler warnings by adding explicit 'new' keyword to hiding members

- Add 'new' modifier to Empty properties in BlockGridModel, BlockListModel, RichTextBlockModel
- Add 'new' modifier to DeepCloneWithResetIdentities in IContentType, IMediaType
- Add 'new' modifier to Save/GetById methods in IContentService, IMediaService, IMemberService
- Add 'new' modifier to EFCore interface members (IAmbientEFCoreScopeStack, IEFCoreScope)
- Add 'new' modifier to ExternalLoginSignInResult.NotAllowed and CreateMediaTypeRequestModel.Collection
- Improve XML documentation for edited public members

Note: CS0114 warnings (virtual/override) were intentionally not fixed as they may be breaking changes. Will add more details to the PR.

* fix: Resolve CS0108 compiler warnings by adding explicit 'new' keyword

fix: Resolve CS0108 compiler warnings by adding explicit 'new' keyword to hiding members

- Add 'new' modifier to Empty properties in BlockGridModel, BlockListModel, RichTextBlockModel
- Add 'new' modifier to DeepCloneWithResetIdentities in IContentType, IMediaType
- Add 'new' modifier to Save/GetById methods in IContentService, IMediaService, IMemberService
- Add 'new' modifier to EFCore interface members (IAmbientEFCoreScopeStack, IEFCoreScope)
- Add 'new' modifier to ExternalLoginSignInResult.NotAllowed and CreateMediaTypeRequestModel.Collection
- Improve XML documentation for edited public members

Note: CS0114 warnings (virtual/override) were intentionally not fixed as they may be breaking changes. Will add more details to the PR.

* Modifying the PR based on feedback from Andy.

Files Modified (Removed Duplicate Members)
src/Umbraco.Cms.Api.Management/ViewModels/MediaType/CreateMediaTypeRequestModel.cs
Removed duplicate Collection property (already defined in base class ContentTypeModelBase)
src/Umbraco.Core/Services/IContentService.cs
Removed duplicate GetById(Guid key) method (already in IContentServiceBase<IContent>)
Removed duplicate Save(IEnumerable<IContent> contents, ...) method (already in IContentServiceBase<IContent>)
src/Umbraco.Core/Services/IMediaService.cs
Removed duplicate GetById(Guid key) method (already in IContentServiceBase<IMedia>)
Removed duplicate Save(IEnumerable<IMedia> medias, ...) method (already in IContentServiceBase<IMedia>)
src/Umbraco.Core/Services/IMemberService.cs
Removed duplicate Save(IEnumerable<IMember> members, ...) method (already in IContentServiceBase<IMember>)
Files Left Unchanged (Keeping new keyword)
The following files were correctly fixed with the new keyword because they intentionally hide base members to return more specific types:
BlockGridModel.cs, BlockListModel.cs, RichTextBlockModel.cs - Empty returns specific type
IContentType.cs, IMediaType.cs - DeepCloneWithResetIdentities returns specific interface
ExternalLoginSignInResult.cs - NotAllowed returns ExternalLoginSignInResult instead of SignInResult
IEFCoreScope.cs, IAmbientEfCoreScopeStack.cs - re-declarations for documentation purposes

* fix: Resolve CS0108 compiler warnings by adding explicit 'new' keyword

fix: Resolve CS0108 compiler warnings by adding explicit 'new' keyword to hiding members

- Add 'new' modifier to Empty properties in BlockGridModel, BlockListModel, RichTextBlockModel
- Add 'new' modifier to DeepCloneWithResetIdentities in IContentType, IMediaType
- Add 'new' modifier to Save/GetById methods in IContentService, IMediaService, IMemberService
- Add 'new' modifier to EFCore interface members (IAmbientEFCoreScopeStack, IEFCoreScope)
- Add 'new' modifier to ExternalLoginSignInResult.NotAllowed and CreateMediaTypeRequestModel.Collection
- Improve XML documentation for edited public members

Note: CS0114 warnings (virtual/override) were intentionally not fixed as they may be breaking changes. Will add more details to the PR.

* Modifying the PR based on feedback from Andy.

Files Modified (Removed Duplicate Members)
src/Umbraco.Cms.Api.Management/ViewModels/MediaType/CreateMediaTypeRequestModel.cs
Removed duplicate Collection property (already defined in base class ContentTypeModelBase)
src/Umbraco.Core/Services/IContentService.cs
Removed duplicate GetById(Guid key) method (already in IContentServiceBase<IContent>)
Removed duplicate Save(IEnumerable<IContent> contents, ...) method (already in IContentServiceBase<IContent>)
src/Umbraco.Core/Services/IMediaService.cs
Removed duplicate GetById(Guid key) method (already in IContentServiceBase<IMedia>)
Removed duplicate Save(IEnumerable<IMedia> medias, ...) method (already in IContentServiceBase<IMedia>)
src/Umbraco.Core/Services/IMemberService.cs
Removed duplicate Save(IEnumerable<IMember> members, ...) method (already in IContentServiceBase<IMember>)
Files Left Unchanged (Keeping new keyword)
The following files were correctly fixed with the new keyword because they intentionally hide base members to return more specific types:
BlockGridModel.cs, BlockListModel.cs, RichTextBlockModel.cs - Empty returns specific type
IContentType.cs, IMediaType.cs - DeepCloneWithResetIdentities returns specific interface
ExternalLoginSignInResult.cs - NotAllowed returns ExternalLoginSignInResult instead of SignInResult
IEFCoreScope.cs, IAmbientEfCoreScopeStack.cs - re-declarations for documentation purposes

* Apply suggestions from code review

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

* Reverted the one of the changes and updated some XML doc tags.

* Should have committed these files.

Corrected a name space in the test files.

* Remove warning on missing access modifiers on interface members.

* Revert breaking namespace change.

* Fixed build errors following namespace reversion.

---------

Co-authored-by: Andy Butland <abutland73@gmail.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-12-24 13:11:03 +01:00
8a47072b9b Code quality: Replace obsolete APIs in Umbraco.TestData controllers (#21251)
* fix(tests): Replace obsolete APIs in Umbraco.TestData controllers

Replaced deprecated synchronous service methods with their modern async
equivalents in the TestData controllers to eliminate CS0618 warnings.

Changes:
- LoadTestController: Replace IFileService with ITemplateService,
  use IDataTypeService.GetAsync(Guid) instead of GetDataType(int),
  use IContentTypeService.CreateAsync() instead of Save()
- SegmentTestController: Use IContentTypeService.UpdateAsync()
  instead of Save()
- UmbracoTestDataController: Use IContentTypeService.CreateAsync()
  and UpdateAsync() instead of Save()

Also adds comprehensive XML documentation to all three controllers
including class summaries, constructor parameters, and method
documentation.

The controllers now use:
- Constants.DataTypes.Guids.TextstringGuid instead of magic int -88
- Constants.Security.SuperUserKey for async service operations
- Task<IActionResult> return types where async operations are used

* Apply suggestions from code review

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

* Further amends from code review.

---------

Co-authored-by: Andy Butland <abutland73@gmail.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-12-24 09:36:45 +00:00
Chris HoustonandGitHub 6726e6b208 Benchmarks: Fix obsolete BenchmarkDotNet API warnings by replacing depreciated function calls. (#21250)
* fix(benchmarks): resolve obsolete BenchmarkDotNet API warnings

Update BenchmarkDotNet configuration to use current API methods:

- Replace deprecated `ManualConfig.Add()` with `AddDiagnoser()` for memory diagnostics
- Replace deprecated `ConfigExtensions.With()` with `AddJob()` for job configuration
- Initialize `_totalItemCount` field to suppress CS0649 warning

These changes resolve 4 compiler warnings (CS0618, CS0649) in the benchmark project
without any functional changes.

* Removing the benchmark artifacts and adding the folder to gitignore
2025-12-24 10:25:35 +01:00
27209c2295 Code documentation: Added XML Docs to the files in the Umbraco.Cms.Imaging.ImageSharp2 project (#21249)
* Added XML Docs to the files in Umbraco.Cms.Imaging.ImageSharp2

This commit fixes the missing XML documentation with the Umbraco.Cms.Imaging.ImageSharp2 project.

Now this project should have no build warnings.

* Update src/Umbraco.Cms.Imaging.ImageSharp2/UmbracoBuilderExtensions.cs

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

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-12-24 09:34:56 +01:00
Chris HoustonandGitHub 6c40dc0d25 Added XML Docs to the files in the Umbraco.Cms.Imaging.ImageSharp (#21248)
The only build errors in this project were missing XML documentation, so I've now updated each of the files.
2025-12-24 09:33:27 +01:00
Chris HoustonandGitHub fe06cd28bf Code Quality: Fixing the build warnings in the Umbraco.Tests.Common project (#21245)
* Fixing the build warnings in the Umbraco.Tests.Common Project

The main warning was about the file name not matching the class, this was because an interface was being defined first within the class file.

This should either be moved into its own file, or to the bottom of the file to fix this warning. Rather than creating a new file, I've moved the interface to the bottom, but if you'd prefer a new file, just let me know :)

Also removed the TODO in the project file and the WarningsNotAsErrors as they have all been resolved.

* Moved the interface into it's own file.

* Removed the interfaces folder.
2025-12-23 18:11:51 +00:00
Chris HoustonandGitHub 4083ceb63a fix: Update RenderNoContentController to use non-obsolete constructor (#21246)
- Replace obsolete constructor call that used IUmbracoContextAccessor
- Use new constructor with IDocumentUrlService instead
- Add using for Umbraco.Cms.Core.Services namespace
- Remove unused Umbraco.Cms.Core.Web namespace

The obsolete constructor was scheduled for removal in Umbraco 18.
This is an internal change only - ControllersAsServicesComposer is not
shipped with Umbraco.Templates package.
2025-12-23 16:07:52 +00:00
69e7151079 Media Picker: Always include folders when searching for media (#21216)
* Always include folders when searching for media. Closes #21149

* Address copilot comments

* Resolved breaking changes.

---------

Co-authored-by: Andy Butland <abutland73@gmail.com>
2025-12-23 10:16:32 +00:00
Chris HoustonandGitHub 74c71f977c Fixes all SA1117 warnings and adding more XML documentation comments. (#21224)
This commit fixes around 1,000 build warnings related to SA1117, this warning is related to ensuring each parameter passed into a function is on a new line OR all on one line.

I have also added XML code comments to any public function within these files and fixed some that had invalid / old comments.
2025-12-23 10:34:40 +01:00
Niels Lyngsø f12da6e4cb Merge branch 'v16/dev' 2025-12-22 13:25:22 +01:00
Niels Lyngsø c19cc92690 optical space adjustment for content-type-designer property 2025-12-22 13:25:06 +01:00
396ebdd48d HybridCache: Clear published content cache on content type change (#21225)
* Clear the cacje level published content cache on content type change.

* Apply suggestions from code review

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

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-12-22 15:14:21 +09:00
e91c2e659d User Avatar: The "Change Photo" button is not working in all cases (#21206)
fix(user): fix avatar change and remove functionality

- Fix "Change photo" button not working on subsequent clicks by using
  { once: true } on the event listener and resetting the input value
- Fix avatar not disappearing after removal by clearing _imgSrc when
  imgUrls is set to empty array

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-20 09:55:25 +01:00
Andy Butland 5bb265b3a4 Merge branch 'release/17.1' 2025-12-20 09:38:59 +01:00
Chris HoustonandGitHub 107fb48002 Cryptographic Functions: Fixes obsolete .NET API warnings (SYSLIB0023, SYSLIB0045, SYSLIB0021, SYSLIB0013, SYSLIB0012) by replacing deprecated cryptographic and reflection APIs with their modern equivalents. (#21213)
Fixed various obsolete .NET API calls.

These all show up as warnings when you try to build the Umbraco solution:

- SYSLIB0045 (6) - Use proper HashAlgorithm creation
- SYSLIB0023 (4) - Replace RNGCryptoServiceProvider with RandomNumberGenerator
- SYSLIB0021 (8) - Replace deprecated crypto types
- SYSLIB0013 (4) - Replace Uri.EscapeUriString
2025-12-19 21:19:41 +01:00
Chris HoustonandGitHub 7b449658d3 Fix build warning CS0252 - unintended reference comparison in tests (#21212)
Fix unintended reference comparison in tests

Explicitly casts the mock property to string in a predicate to prevent reference equality checks. Adds constraints for storage type and editor to align with expected data characteristics and improve test reliability. Keeps mock service behavior unchanged; minor formatting tweak included.
2025-12-19 21:06:55 +01:00
add5bd31c9 Management API: Fixes the warning CS8524 and tidying up the "Set Status Redirect Url Management Controller" (#21211)
* Fixes the warning CS8524 and tidying up the SetStatusRedirectUrlManagementController

I have added throwing an error in the switch statement if neither of the know enums is submitted to the API, in theory this should never happen.

The other option would be to simply return false, but I think throwing the exception is better as it will alert whoever is calling the API that their call is invalid.

* Update src/Umbraco.Cms.Api.Management/Controllers/RedirectUrlManagement/SetStatusRedirectUrlManagementController.cs

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

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-12-19 21:04:53 +01:00
Chris HoustonandGitHub 72447b040a Resolves all CA2017 warnings and improves log message clarity (#21210)
Improves log message clarity

- Refines a warning when a property type alias is missing, clarifying that a default value is returned.
- Improves backoffice token revocation log by including contextual client id for easier troubleshooting.
- Corrects model generation error logging by passing the exception as the first argument for consistency.
2025-12-19 18:31:45 +01:00
Chris HoustonandGitHub b13544083f Docs: Fix CS1573 warnings - add missing XML param tags (#21209)
Add missing parameter documentation tags to resolve CS1573 compiler warnings:

- Add cancellationToken param tags to 24 API Management controllers

- Add payloadType param tags to 6 Webhook extension files

- Fix various missing param tags in Core and Infrastructure files
2025-12-19 18:25:08 +01:00
Nhu DinhandGitHub 70d1728843 E2E: QA Added acceptance tests for content delivery API (#21095)
* Added tests for media delivery api

* Added tests for content delivery api

* Fixed import

* Added tests for Content Delivery API

* Updated skip tag and issue link for the failing tests

* Bumped version

* Refactor code for content delivery api tests

* Moved repeat steps to beforeEach and added more waits

* Bumped version

* Added more waits

* Updated variable names

* Grouped tests

* Renamed

* Fixed names
2025-12-19 10:16:19 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>Jacob Overgaard
c08e39064f Storybook: Bumps storybook from 9.0.14 to 10.1.10 (#21208)
* Bump storybook

Bumps the npm_and_yarn group with 1 update in the /src/Umbraco.Web.UI.Client directory: [storybook](https://github.com/storybookjs/storybook/tree/HEAD/code/core).


Updates `storybook` from 9.0.14 to 9.1.17
- [Release notes](https://github.com/storybookjs/storybook/releases)
- [Changelog](https://github.com/storybookjs/storybook/blob/next/CHANGELOG.md)
- [Commits](https://github.com/storybookjs/storybook/commits/v9.1.17/code/core)

---
updated-dependencies:
- dependency-name: storybook
  dependency-version: 9.1.17
  dependency-type: direct:development
  dependency-group: npm_and_yarn
...

Signed-off-by: dependabot[bot] <support@github.com>

* build(deps-dev): bumps storybook from v9 to v10

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com>
2025-12-19 10:13:31 +00:00
Nhu DinhandGitHub dfb7507ae2 E2E: QA Added acceptance tests for rendering content with invariant blocks (#21180)
* Added tests for rendering content with block list

* Updated tests for rendering content with block lists

* Updated message when has no block lists

* Added tests for rendering content with block grid

* Updated code

* Bumped version

* Changed npm command to make tests run in the pipeline

* Fixed comment

* Reverted command
2025-12-19 09:25:17 +00:00
d749af8974 Upgrade MSW from 1.3.5 to 2.12.4 (#21096)
* upgrade msw, migrate all interceptors, and update backoffice integration

* Fix msw test runner integration

---------

Co-authored-by: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com>
2025-12-19 10:19:10 +01:00
Nhu DinhandGitHub 14fe6f4888 E2E: QA Fixed failing tests for the current user profile (#21214)
* Added more waits

* Make tests run in the pipeline

* Reverted npm command
2025-12-19 09:18:45 +00:00
Chris HoustonandGitHub 0d4f24300a fix(code-quality): resolve CS0628 warnings - change protected to private in sealed classes. (#21193)
* fix(code-quality): resolve CS0628 warnings - change protected to private in sealed classes

Changed protected members to private in sealed classes across 26 files. Protected members in sealed classes serve no purpose since sealed classes cannot be inherited. This eliminates 57 CS0628 compiler warnings.

* fix: use public for NUnit SetUp methods per Copilot review

NUnit requires SetUp methods to be at least protected. Using public
avoids CS0628 while allowing NUnit to discover and execute the methods.

* Clean up

- Renames internal fields to clearer names and aligns with conventions
- Changes internal cache holder to use auto-properties for state ( fixes another build warning )
- Removes an unused exception type from the loader and cleans up unused usings
- Adds "Umbraco" to the list of known spellings in .vsCode settings file, amazed this wasn't already there :)

* Improvements to fix CodeScene Code Health Review issues.

- Introduces debug-only logging helpers and routes all log messages through them for consistency
- Centralizes retrieval of discoverable types and scanning logic to simplify paths
- Aligns logging of cached vs non-cached and slow paths with new helpers
- Documents data-holding structure used to store type lists for clarity

* Refactoring to make CodeScene happy, removing code duplication :)
2025-12-18 19:28:13 +01:00
Engiber LozadaandGitHub ebb6590bad Data Types: Add condition to hide delete action in non deletable data types. (#21184)
* Created a condition that check non deletable data types.

* Export condition.

* Updated export path.
2025-12-18 09:58:29 +00:00
Niels LyngsøandGitHub 7b85109606 News Dashboard: Update styling to fit with new style (#21185)
styling of the dashboard
2025-12-18 09:51:11 +01:00
Chris HoustonandGitHub 0e60602746 Tests: Fix CS4014 warnings - add missing await operators (#21194)
Fix unawaited async calls in tests

- Converts setup to async and awaits data creation
- Awaits operation status update to fix potential unawaited calls
- Updates nested validation tests to use await for helper results
- Removes stray BOM character in a test file
- Improves overall async flow, addressing CS4014
Relates to CS4014
2025-12-18 07:03:23 +01:00
Mads RasmussenandGitHub e08a60e74d Performance: Embed Store API in Manifests to lower number of network request (#21191)
Refactor manifests to use direct store imports to minimize the number of lazy loaded items on startup
2025-12-17 23:13:39 +01:00
Niels LyngsøandGitHub 4c7abc41b2 Performance: Bundle Js Libs (#21187)
bundle libs
2025-12-17 21:41:58 +00:00
Niels LyngsøandGitHub 45ae5aaac0 Performance: Embeds the API of selected extra Conditions (#21188)
embeds the API of selected Conditions
2025-12-17 21:43:08 +01:00
Niels LyngsøandGitHub 30d9288f12 Culture and Hostnames: Load all languages (#21169)
make sure we take all languages
2025-12-17 16:01:20 +01:00
Niels Lyngsø e27b661b3f update version in package lock 2025-12-17 15:51:12 +01:00
Kenn JacobsenandGitHub 75e4b2b18d Indexing: Gracefully handle element property variance changes at index time (#21183)
Gracefully handle element property variance changes at index time
2025-12-17 15:44:09 +01:00
8642b9e615 Content Types: Fix property variation change when content exists only in non-default language (#21182)
* Content Types: Fix property variation change when content exists only in non-default language (closes #11771)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* Add tests for AllowEditInvariantFromNonDefault enabled scenario

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-17 15:21:17 +01:00
638e181334 Content Types: Fix property variation change when content exists only in non-default language (#21182)
* Content Types: Fix property variation change when content exists only in non-default language (closes #11771)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* Add tests for AllowEditInvariantFromNonDefault enabled scenario

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-17 15:18:14 +01:00
254400bb58 Block level variance: fix values being polluted when changing variance before publish (#21121)
* TDD solution to Block level variance publishing changing to no variance retaining block level variance values and vica versa

* Add similar tests for the other block editors

* Apply suggestions from code review

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

* Amend the test scenarios

* Simplify the merge clean-up to remove all misaligned values.

* Fix failing test (remove false assumptions)

---------

Co-authored-by: Andy Butland <abutland73@gmail.com>
Co-authored-by: Kenn Jacobsen <kja@umbraco.dk>
2025-12-17 13:51:19 +01:00
3bcdcc56c8 Cache: Add null checks for entities that may no longer exist during cache refresh (#21181)
* Add null checks in case content no longer exists

* Add logging statement

* Add integration tests for null handling in cache refresh services

Tests verify that DocumentUrlService and PublishStatusService
do not throw exceptions when called with non-existent content keys,
which can happen when processing stale cache instructions.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* Add integration tests for stale cache instruction handling

Tests simulate the scenario where a cache instruction is processed
for content that has been deleted (stale instruction). This can happen when:
1. Content is saved (cache instruction queued)
2. Content is deleted before instruction is processed
3. Server restarts and processes the stale instruction

Tests cover:
- ContentCacheRefresher with RefreshBranch for deleted content
- ContentCacheRefresher with RefreshNode for deleted content
- MediaCacheRefresher with RefreshBranch for deleted media
- Processing instructions for content that never existed

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-17 11:44:07 +01:00
Nhu DinhandGitHub dd6ae8bd25 E2E: QA Added acceptance tests for removing a not-found content picker (#21177)
* Added tests for removing a not-found content picker

* Added comment

* Bumped version

* Change npm command to make tests run in the pipeline

* Reverted npm command
2025-12-17 09:38:39 +00:00
Andreas ZerbstandGitHub aa32035830 E2E QA: Updated integration test that was missing directory setup (#21167)
* Added base class

* Added implementation of base class

* Added missing parameter

* Added try-catch around delete operations in Purge() to ignore locked files during cleanup.
2025-12-17 09:57:17 +01:00
911d2fe67b Content: Fix name() and getName() to use active variant (closes #20759) (#21171)
* Content: Fix name() and getName() to use active variant when no variantId provided

When calling `name()` or `getName()` without a variantId argument, the methods
now correctly return the name of the first active variant from the split view
instead of always returning the first variant in the data array.

The `name()` method now returns a reactive observable that updates when the
active variant changes, using `mergeObservables` to combine the split view's
active variant observable with the variants data.

The `getName()` method now uses `splitView.getActiveVariants()[0]` to get the
current active variant synchronously, with a fallback to the first variant if
no active variant is set.

This fixes an issue where block previews could not reactively observe the
document name because the callback parameter was always empty.

Closes #20759

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* chore: use UmbVariantId.compare() for consistent variant matching

Address PR review feedback by using UmbVariantId.Create() and compare()
instead of direct property comparison. This ensures consistent behavior
with the existing variant comparison pattern used throughout the codebase.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* chore: formatting

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-17 08:48:42 +01:00
Sven GeusensandGitHub 81685bf0e2 Added a migration to remove the property regex validation length limit (#21175) 2025-12-17 06:48:36 +01:00
Andy Butland 9fb018276a Bumped version to 17.2.0-rc. 2025-12-17 06:41:09 +01:00
Andreas ZerbstandGitHub 2a604c8719 E2E: QA Replaced unreliable Thread.Sleep(500) with a counter/gate pattern that ensures both transactions are initialized before releasing them to compete for locks (#21165)
Fix flaky test
2025-12-16 12:02:07 +00:00
788 changed files with 14691 additions and 10844 deletions
+1 -1
View File
@@ -102,7 +102,7 @@ dotnet_style_predefined_type_for_locals_parameters_members = true:warning
dotnet_style_predefined_type_for_member_access = true:warning
# Modifier preferences
# https://docs.microsoft.com/visualstudio/ide/editorconfig-language-conventions#normalize-modifiers
dotnet_style_require_accessibility_modifiers = always:warning
dotnet_style_require_accessibility_modifiers = for_non_interface_members:warning
csharp_preferred_modifier_order = public,private,protected,internal,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,volatile,async:warning
visual_basic_preferred_modifier_order = Partial,Default,Private,Protected,Public,Friend,NotOverridable,Overridable,MustOverride,Overloads,Overrides,MustInherit,NotInheritable,Static,Shared,Shadows,ReadOnly,WriteOnly,Dim,Const,WithEvents,Widening,Narrowing,Custom,Async:warning
dotnet_style_readonly_field = true:warning
+1 -1
View File
@@ -30,7 +30,7 @@ This guide describes each step to make your first contribution:
Create a new branch based on `main` and name it after the issue you're fixing. For example: `v15/bugfix/18132-rte-tinymce-onchange-value-check`.
Please follow this format for branches: `v{major}/{feature|bugfix|task}/{issue}-{description}`.
Please follow this format for branches: `v{major}/{feature|bugfix|task|qa|improvement}/{issue}-{description}`.
This is a development branch for the particular issue you're working on, in this case, a bug-fix for issue number `18132` that affects Umbraco v.15.
+4 -2
View File
@@ -95,6 +95,10 @@ tools/docfx/
/tests/Umbraco.Tests.Integration/[Uu]mbraco/[Ll]ogs/
/tests/Umbraco.Tests.Integration/Views/
/tests/Umbraco.Tests.UnitTests/[Uu]mbraco/[Dd]ata/TEMP/
/BenchmarkDotNet.Artifacts/
playwright-report
trace.zip
/tests/Umbraco.Tests.AcceptanceTest/results
# Ignore auto-generated schema
/src/Umbraco.Cms.Targets/tasks/
@@ -107,5 +111,3 @@ tools/docfx/
/tests/Umbraco.Tests.Integration/appsettings-schema.*.json
/tests/Umbraco.Tests.Integration/umbraco-package-schema.json
/src/Umbraco.Cms/appsettings-schema.json
playwright-report
trace.zip
+1
View File
@@ -3,6 +3,7 @@
"backoffice",
"pickable",
"Pickable",
"Umbraco",
"unprovide",
"Unproviding"
],
+27 -2
View File
@@ -142,8 +142,33 @@ Web.UI → Web.Common → Infrastructure → Core
### Branching Strategy
- **Main branch**: `main` (protected)
- **Branch naming**:
- See `.github/CONTRIBUTING.md` for full guidelines
- **Branch naming convention**: `v<version>/<type>/<description>`
**Format**: `v{major-version}/{type}/{kebab-case-description}`
**Version**: Read from `version.json` in the repository root. Use the major version number (e.g., `v17` for version 17.x.x).
**Types**:
| Type | Use Case |
|------|----------|
| `feature` | New feature being introduced to the product |
| `bugfix` | Fix to an existing issue with the product |
| `qa` | Adding or updating unit, integration, or end-to-end tests |
| `improvement` | Update to something that already exists but isn't broken (UI finessing, refactoring) |
| `task` | Update that doesn't directly impact product behavior (dependency updates, build pipeline) |
**Description**: A short, kebab-case description (a few words). This should be prefixed with the GitHub issue number if the update is related to resolving a tracked issue.
**Examples**:
```
v17/bugfix/12345-correct-display-of-pending-migrations
v17/feature/add-webhook-support
v17/improvement/optimize-content-cache
v17/qa/add-media-service-tests
v17/task/update-ef-core-dependency
```
See `.github/CONTRIBUTING.md` for full guidelines.
### Pull Request Process
+29 -20
View File
@@ -2,6 +2,7 @@
<Project>
<PropertyGroup>
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
<CentralPackageTransitivePinningEnabled>true</CentralPackageTransitivePinningEnabled>
</PropertyGroup>
<!-- Global packages (private, build-time packages for all projects) -->
<ItemGroup>
@@ -12,27 +13,27 @@
</ItemGroup>
<!-- Microsoft packages -->
<ItemGroup>
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation" Version="10.0.0" />
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation" Version="10.0.1" />
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="4.14.0" />
<PackageVersion Include="Microsoft.Data.Sqlite" Version="10.0.0" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="10.0.0" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.SqlServer" Version="10.0.0" />
<PackageVersion Include="Microsoft.Extensions.Caching.Abstractions" Version="10.0.0" />
<PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="10.0.0" />
<PackageVersion Include="Microsoft.Extensions.Configuration.Abstractions" Version="10.0.0" />
<PackageVersion Include="Microsoft.Extensions.Configuration.Json" Version="10.0.0" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="10.0.0" />
<PackageVersion Include="Microsoft.Extensions.FileProviders.Embedded" Version="10.0.0" />
<PackageVersion Include="Microsoft.Extensions.FileProviders.Physical" Version="10.0.0" />
<PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="10.0.0" />
<PackageVersion Include="Microsoft.Extensions.Http" Version="10.0.0" />
<PackageVersion Include="Microsoft.Extensions.Identity.Core" Version="10.0.0" />
<PackageVersion Include="Microsoft.Extensions.Identity.Stores" Version="10.0.0" />
<PackageVersion Include="Microsoft.Extensions.Logging" Version="10.0.0" />
<PackageVersion Include="Microsoft.Extensions.Options" Version="10.0.0" />
<PackageVersion Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="10.0.0" />
<PackageVersion Include="Microsoft.Extensions.Options.DataAnnotations" Version="10.0.0" />
<PackageVersion Include="Microsoft.Extensions.Caching.Hybrid" Version="10.0.0" />
<PackageVersion Include="Microsoft.Data.Sqlite" Version="10.0.1" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="10.0.1" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.SqlServer" Version="10.0.1" />
<PackageVersion Include="Microsoft.Extensions.Caching.Abstractions" Version="10.0.1" />
<PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="10.0.1" />
<PackageVersion Include="Microsoft.Extensions.Configuration.Abstractions" Version="10.0.1" />
<PackageVersion Include="Microsoft.Extensions.Configuration.Json" Version="10.0.1" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="10.0.1" />
<PackageVersion Include="Microsoft.Extensions.FileProviders.Embedded" Version="10.0.1" />
<PackageVersion Include="Microsoft.Extensions.FileProviders.Physical" Version="10.0.1" />
<PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="10.0.1" />
<PackageVersion Include="Microsoft.Extensions.Http" Version="10.0.1" />
<PackageVersion Include="Microsoft.Extensions.Identity.Core" Version="10.0.1" />
<PackageVersion Include="Microsoft.Extensions.Identity.Stores" Version="10.0.1" />
<PackageVersion Include="Microsoft.Extensions.Logging" Version="10.0.1" />
<PackageVersion Include="Microsoft.Extensions.Options" Version="10.0.1" />
<PackageVersion Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="10.0.1" />
<PackageVersion Include="Microsoft.Extensions.Options.DataAnnotations" Version="10.0.1" />
<PackageVersion Include="Microsoft.Extensions.Caching.Hybrid" Version="10.1.0" />
<PackageVersion Include="System.Linq.Async" Version="7.0.0" />
</ItemGroup>
<!-- Umbraco packages -->
@@ -50,6 +51,7 @@
<PackageVersion Include="JsonPatch.Net" Version="3.3.0" />
<PackageVersion Include="K4os.Compression.LZ4" Version="1.3.8" />
<PackageVersion Include="MailKit" Version="4.14.1" />
<PackageVersion Include="Markdig" Version="0.44.0" />
<PackageVersion Include="Markdown" Version="2.2.1" />
<PackageVersion Include="MessagePack" Version="3.1.4" />
<PackageVersion Include="MiniProfiler.AspNetCore.Mvc" Version="4.5.4" />
@@ -78,5 +80,12 @@
</ItemGroup>
<!-- Transitive pinned versions (only required because our direct dependencies have vulnerable versions of transitive dependencies) -->
<ItemGroup>
<!-- Dazinator.Extensions.FileProviders references vulnerable versions of the following: -->
<!-- TODO (V18): Remove these pinned dependencies when the Dazinator.Extensions.FileProviders dependency is removed. -->
<PackageVersion Include="System.Net.Http" Version="4.3.4" />
<PackageVersion Include="System.Private.Uri" Version="4.3.2" />
<!-- Markdown references vulnerable version of the following: -->
<!-- TODO (V19): Remove these pinned dependencies when the Markdown dependency is removed. -->
<PackageVersion Include="System.Text.RegularExpressions" Version="4.3.1" />
</ItemGroup>
</Project>
+8
View File
@@ -196,6 +196,14 @@ Copyright: 2013-2024 .NET Foundation and Contributors
---
Markdig: A fast, powerful, CommonMark compliant, extensible Markdown processor for .NET
URL: https://github.com/xoofx/markdig
License: BSD-2-Clause license
Copyright: 2018+, Alexandre Mutel. All rights reserved.
---
Markdown: A library for parsing and compiling Markdown
URL: https://github.com/hey-red/Markdown
+1 -1
View File
@@ -215,7 +215,7 @@ stages:
inputs:
targetType: inline
script: |
choco install docfx --version=2.59.4 -y
dotnet tool install -g docfx
if ($lastexitcode -ne 0){
throw ("Error installing DocFX")
}
+25 -2
View File
@@ -28,6 +28,12 @@ parameters:
type: boolean
default: false
# Can we slow our tests down when running on SQLite? That way we might be able to avoid DB locks
- name: skipSqliteAcceptanceTests
displayName: Skip SQLite acceptance tests
type: boolean
default: true
variables:
nodeVersion: 20
solution: umbraco.sln
@@ -342,7 +348,7 @@ stages:
- job:
displayName: E2E Tests (SQLite)
timeoutInMinutes: 180
condition: ${{ eq(parameters.skipDefaultConfigAcceptanceTests, false) }}
condition: ${{ and(eq(parameters.skipDefaultConfigAcceptanceTests, false), eq(parameters.skipSqliteAcceptanceTests, false)) }}
variables:
# Connection string
CONNECTIONSTRINGS__UMBRACODBDSN: Data Source=Umbraco;Mode=Memory;Cache=Shared;Foreign Keys=True;Pooling=True
@@ -581,6 +587,23 @@ stages:
CONNECTIONSTRINGS__UMBRACODBDSN: Server=(local);Database=Umbraco;User Id=sa;Password=$(SA_PASSWORD);Encrypt=True;TrustServerCertificate=True
CONNECTIONSTRINGS__UMBRACODBDSN_PROVIDERNAME: Microsoft.Data.SqlClient
additionalEnvironmentVariables: false
# ContentSettingConfig
WindowsContentSettingsConfig:
vmImage: "windows-latest"
testFolder: "ContentSettingConfig"
port: ''
testCommand: "npx playwright test --project=contentSettingConfig"
CONNECTIONSTRINGS__UMBRACODBDSN: Data Source=(localdb)\MSSQLLocalDB;AttachDbFilename=|DataDirectory|\Umbraco.mdf;Integrated Security=True
CONNECTIONSTRINGS__UMBRACODBDSN_PROVIDERNAME: Microsoft.Data.SqlClient
additionalEnvironmentVariables: false
LinuxContentSettingsConfig:
vmImage: "ubuntu-latest"
testFolder: "ContentSettingConfig"
port: ''
testCommand: "npx playwright test --project=contentSettingConfig"
CONNECTIONSTRINGS__UMBRACODBDSN: Server=(local);Database=Umbraco;User Id=sa;Password=$(SA_PASSWORD);Encrypt=True;TrustServerCertificate=True
CONNECTIONSTRINGS__UMBRACODBDSN_PROVIDERNAME: Microsoft.Data.SqlClient
additionalEnvironmentVariables: false
# SMTP
LinuxSMTP:
vmImage: "ubuntu-latest"
@@ -693,7 +716,7 @@ stages:
AZUREB2CTESTUSEREMAIL: $(AZUREB2CTESTUSEREMAIL)
AZUREB2CTESTUSERPASSWORD: $(AZUREB2CTESTUSERPASSWORD)
DatabaseType: ${{ variables.DatabaseType }}
# Stop SMTP4dev container
- bash: |
echo "Stopping SMTP4dev container..."
@@ -1,13 +1,10 @@
using Asp.Versioning;
using Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.AspNetCore.Mvc.ApiExplorer;
using Microsoft.AspNetCore.Mvc.Controllers;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using Microsoft.OpenApi;
using Swashbuckle.AspNetCore.SwaggerGen;
using Umbraco.Cms.Api.Common.OpenApi;
using Umbraco.Extensions;
using Umbraco.Cms.Core.DependencyInjection;
namespace Umbraco.Cms.Api.Common.Configuration;
@@ -16,15 +13,31 @@ public class ConfigureUmbracoSwaggerGenOptions : IConfigureOptions<SwaggerGenOpt
private readonly IOperationIdSelector _operationIdSelector;
private readonly ISchemaIdSelector _schemaIdSelector;
private readonly ISubTypesSelector _subTypesSelector;
private readonly IDocumentInclusionSelector _documentInclusionSelector;
public ConfigureUmbracoSwaggerGenOptions(
IOperationIdSelector operationIdSelector,
ISchemaIdSelector schemaIdSelector,
ISubTypesSelector subTypesSelector)
ISubTypesSelector subTypesSelector,
IDocumentInclusionSelector documentInclusionSelector)
{
_operationIdSelector = operationIdSelector;
_schemaIdSelector = schemaIdSelector;
_subTypesSelector = subTypesSelector;
_documentInclusionSelector = documentInclusionSelector;
}
[Obsolete("Please use the constructor with all parameters. Scheduled for removal in Umbraco 19.")]
public ConfigureUmbracoSwaggerGenOptions(
IOperationIdSelector operationIdSelector,
ISchemaIdSelector schemaIdSelector,
ISubTypesSelector subTypesSelector)
: this(
operationIdSelector,
schemaIdSelector,
subTypesSelector,
StaticServiceProvider.Instance.GetRequiredService<IDocumentInclusionSelector>())
{
}
public void Configure(SwaggerGenOptions swaggerGenOptions)
@@ -39,19 +52,11 @@ public class ConfigureUmbracoSwaggerGenOptions : IConfigureOptions<SwaggerGenOpt
});
swaggerGenOptions.CustomOperationIds(description => _operationIdSelector.OperationId(description));
swaggerGenOptions.DocInclusionPredicate((name, api) =>
{
if (api.ActionDescriptor is ControllerActionDescriptor controllerActionDescriptor
&& controllerActionDescriptor.HasMapToApiAttribute(name))
{
return true;
}
ApiVersionMetadata apiVersionMetadata = api.ActionDescriptor.GetApiVersionMetadata();
return apiVersionMetadata.Name == name
|| (string.IsNullOrEmpty(apiVersionMetadata.Name) && name == DefaultApiConfiguration.ApiName);
});
swaggerGenOptions.TagActionsBy(api => new[] { api.GroupName });
swaggerGenOptions.DocInclusionPredicate(_documentInclusionSelector.Include);
swaggerGenOptions.TagActionsBy(api =>
api.GroupName is null
? []
: new[] { api.GroupName });
swaggerGenOptions.OrderActionsBy(ActionOrderBy);
swaggerGenOptions.SchemaFilter<EnumSchemaFilter>();
swaggerGenOptions.CustomSchemaIds(_schemaIdSelector.SchemaId);
@@ -25,6 +25,7 @@ public static class UmbracoBuilderApiExtensions
builder.Services.AddSingleton<ISchemaIdHandler, SchemaIdHandler>();
builder.Services.AddSingleton<ISubTypesSelector, SubTypesSelector>();
builder.Services.AddSingleton<ISubTypesHandler, SubTypesHandler>();
builder.Services.AddSingleton<IDocumentInclusionSelector, DocumentInclusionSelector>();
builder.Services.Configure<UmbracoPipelineOptions>(options => options.AddFilter(new SwaggerRouteTemplatePipelineFilter("UmbracoApiCommon")));
return builder;
@@ -0,0 +1,30 @@
using Asp.Versioning;
using Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.AspNetCore.Mvc.ApiExplorer;
using Microsoft.AspNetCore.Mvc.Controllers;
using Umbraco.Cms.Api.Common.Configuration;
using Umbraco.Extensions;
namespace Umbraco.Cms.Api.Common.OpenApi;
/// <summary>
/// Determines whether an API description should be included in a specific documentation set based on the document name
/// and API metadata.
/// </summary>
public class DocumentInclusionSelector : IDocumentInclusionSelector
{
/// <inheritdoc/>
public bool Include(string documentName, ApiDescription apiDescription)
{
if (apiDescription.ActionDescriptor is ControllerActionDescriptor controllerActionDescriptor
&& controllerActionDescriptor.HasMapToApiAttribute(documentName))
{
return true;
}
ApiVersionMetadata apiVersionMetadata = apiDescription.ActionDescriptor.GetApiVersionMetadata();
return apiVersionMetadata.Name == documentName
|| (string.IsNullOrEmpty(apiVersionMetadata.Name) && documentName == DefaultApiConfiguration.ApiName);
}
}
@@ -0,0 +1,19 @@
using Microsoft.AspNetCore.Mvc.ApiExplorer;
namespace Umbraco.Cms.Api.Common.OpenApi;
/// <summary>
/// Defines a method that determines whether a given API description should be included in a specific documentation
/// document.
/// </summary>
public interface IDocumentInclusionSelector
{
/// <summary>
/// Determines whether the specified API description should be included in the generated documentation for the given
/// document name.
/// </summary>
/// <param name="documentName">The name of the documentation document being generated.</param>
/// <param name="apiDescription">The API description to evaluate for inclusion.</param>
/// <returns>true if the API description should be included in the documentation; otherwise, false.</returns>
bool Include(string documentName, ApiDescription apiDescription);
}
@@ -99,8 +99,8 @@ public class ByRouteContentApiController : ContentApiItemControllerBase
private IActionResult RedirectTo(IApiContentRoute redirectRoute)
{
Response.Headers.Add("Location-Start-Item-Path", redirectRoute.StartItem.Path);
Response.Headers.Add("Location-Start-Item-Id", redirectRoute.StartItem.Id.ToString("D"));
Response.Headers.Append("Location-Start-Item-Path", redirectRoute.StartItem.Path);
Response.Headers.Append("Location-Start-Item-Id", redirectRoute.StartItem.Id.ToString("D"));
return RedirectPermanent(redirectRoute.Path);
}
}
@@ -7,10 +7,9 @@
<PropertyGroup>
<!--
TODO: Fix and remove overrides:
[ASP0019] use IHeaderDictionary.Append or the indexer to append or set headers
[CS0618/CS0612] update obsolete references
-->
<WarningsNotAsErrors>$(WarningsNotAsErrors),ASP0019,CS0618,CS0612</WarningsNotAsErrors>
<WarningsNotAsErrors>$(WarningsNotAsErrors),CS0618,CS0612</WarningsNotAsErrors>
</PropertyGroup>
<ItemGroup>
@@ -32,6 +32,7 @@ public class ExecuteActionHealthCheckController : HealthCheckControllerBase
/// <summary>
/// Executes a given action from a HealthCheck.
/// </summary>
/// <param name="cancellationToken">The cancellation token.</param>
/// <param name="action">The action to be executed.</param>
/// <returns>The result of a health check after the health check action is performed.</returns>
[HttpPost("execute-action")]
@@ -25,6 +25,7 @@ public class AllHealthCheckGroupController : HealthCheckGroupControllerBase
/// <summary>
/// Gets a paginated grouped list of all names the health checks are grouped by.
/// </summary>
/// <param name="cancellationToken">The cancellation token.</param>
/// <param name="skip">The amount of items to skip.</param>
/// <param name="take">The amount of items to take.</param>
/// <returns>The paged result of health checks group names.</returns>
@@ -25,6 +25,7 @@ public class ByNameHealthCheckGroupController : HealthCheckGroupControllerBase
/// <summary>
/// Gets a health check group with all its health checks by a group name.
/// </summary>
/// <param name="cancellationToken">The cancellation token.</param>
/// <param name="name">The name of the group.</param>
/// <returns>The health check group or not found result.</returns>
[HttpGet("{name}")]
@@ -18,6 +18,7 @@ public class CheckHealthCheckGroupController : HealthCheckGroupControllerBase
/// <summary>
/// Check all health checks in the group with a given group name.
/// </summary>
/// <param name="cancellationToken">The cancellation token.</param>
/// <param name="name">The name of the group.</param>
/// <remarks>The health check result(s) will be included as part of the health checks.</remarks>
/// <returns>The health check group or not found result.</returns>
@@ -24,8 +24,9 @@ public class DetailsIndexerController : IndexerControllerBase
/// <summary>
/// Check if the index has been rebuilt
/// </summary>
/// <param name="indexName"></param>
/// <returns></returns>
/// <param name="cancellationToken">The cancellation token.</param>
/// <param name="indexName">The name of the index.</param>
/// <returns>The index details.</returns>
/// <remarks>
/// This is kind of rudimentary since there's no way we can know that the index has rebuilt, we
/// have a listener for the index op complete so we'll just check if that id is no longer there in the runtime cache
@@ -28,8 +28,9 @@ public class RebuildIndexerController : IndexerControllerBase
/// <summary>
/// Rebuilds the index.
/// </summary>
/// <param name="cancellationToken">The cancellation token.</param>
/// <param name="indexName">The name of the index to rebuild.</param>
/// <returns></returns>
/// <returns>The result of the rebuild operation.</returns>
[HttpPost("{indexName}/rebuild")]
[MapToApiVersion("1.0")]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)]
@@ -28,6 +28,7 @@ public class AllLogViewerController : LogViewerControllerBase
/// <summary>
/// Gets a paginated list of all logs for a specific date range.
/// </summary>
/// <param name="cancellationToken">The cancellation token.</param>
/// <param name="skip">The amount of items to skip.</param>
/// <param name="take">The amount of items to take.</param>
/// <param name="orderDirection">
@@ -27,6 +27,7 @@ public class AllMessageTemplateLogViewerController : LogViewerControllerBase
/// <summary>
/// Gets a paginated list of all log message templates for a specific date range.
/// </summary>
/// <param name="cancellationToken">The cancellation token.</param>
/// <param name="skip">The amount of items to skip.</param>
/// <param name="take">The amount of items to take.</param>
/// <param name="startDate">The start date for the date range (can be null).</param>
@@ -24,6 +24,7 @@ public class AllSinkLevelLogViewerController : LogViewerControllerBase
/// <summary>
/// Gets a paginated list of all loggers' levels.
/// </summary>
/// <param name="cancellationToken">The cancellation token.</param>
/// <param name="skip">The amount of items to skip.</param>
/// <param name="take">The amount of items to take.</param>
/// <returns>The paged result of the configured loggers and their level.</returns>
@@ -25,6 +25,7 @@ public class LogLevelCountLogViewerController : LogViewerControllerBase
/// <summary>
/// Gets the count for each log level from the logs for a specific date range.
/// </summary>
/// <param name="cancellationToken">The cancellation token.</param>
/// <param name="startDate">The start date for the date range (can be null).</param>
/// <param name="endDate">The end date for the date range (can be null).</param>
/// <returns>The log level counts from the (filtered) logs.</returns>
@@ -24,6 +24,7 @@ public class AllSavedSearchLogViewerController : SavedSearchLogViewerControllerB
/// <summary>
/// Gets a paginated list of all saved log searches.
/// </summary>
/// <param name="cancellationToken">The cancellation token.</param>
/// <param name="skip">The amount of items to skip.</param>
/// <param name="take">The amount of items to take.</param>
/// <returns>The paged result of the saved log searches.</returns>
@@ -23,6 +23,7 @@ public class ByNameSavedSearchLogViewerController : SavedSearchLogViewerControll
/// <summary>
/// Gets a saved log search by name.
/// </summary>
/// <param name="cancellationToken">The cancellation token.</param>
/// <param name="name">The name of the saved log search.</param>
/// <returns>The saved log search or not found result.</returns>
[HttpGet("{name}")]
@@ -19,6 +19,7 @@ public class CreateSavedSearchLogViewerController : SavedSearchLogViewerControll
/// <summary>
/// Creates a saved log search.
/// </summary>
/// <param name="cancellationToken">The cancellation token.</param>
/// <param name="savedSearch">The log search to be saved.</param>
/// <returns>The location of the saved log search after the creation.</returns>
[HttpPost]
@@ -18,6 +18,7 @@ public class DeleteSavedSearchLogViewerController : SavedSearchLogViewerControll
/// <summary>
/// Deletes a saved log search with a given name.
/// </summary>
/// <param name="cancellationToken">The cancellation token.</param>
/// <param name="name">The name of the saved log search.</param>
/// <returns>The result of the deletion.</returns>
[HttpDelete("{name}")]
@@ -17,6 +17,7 @@ public class ValidateLogFileSizeLogViewerController : LogViewerControllerBase
/// <summary>
/// Gets a value indicating whether or not you are able to view logs for a specified date range.
/// </summary>
/// <param name="cancellationToken">The cancellation token.</param>
/// <param name="startDate">The start date for the date range (can be null).</param>
/// <param name="endDate">The end date for the date range (can be null).</param>
/// <returns>The boolean result.</returns>
@@ -4,6 +4,7 @@ using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.DependencyInjection;
using Umbraco.Cms.Api.Management.Factories;
using Umbraco.Cms.Api.Management.ViewModels.Media.Item;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.Entities;
@@ -18,16 +19,32 @@ public class SearchMediaItemController : MediaItemControllerBase
private readonly IIndexedEntitySearchService _indexedEntitySearchService;
private readonly IMediaPresentationFactory _mediaPresentationFactory;
private readonly IDataTypeService _dataTypeService;
private readonly IMediaTypeService _mediaTypeService;
[ActivatorUtilitiesConstructor]
public SearchMediaItemController(
IIndexedEntitySearchService indexedEntitySearchService,
IMediaPresentationFactory mediaPresentationFactory,
IDataTypeService dataTypeService)
IDataTypeService dataTypeService,
IMediaTypeService mediaTypeService)
{
_indexedEntitySearchService = indexedEntitySearchService;
_mediaPresentationFactory = mediaPresentationFactory;
_dataTypeService = dataTypeService;
_mediaTypeService = mediaTypeService;
}
[Obsolete("Use the non-obsolete constructor instead, will be removed in Umbraco 18.")]
public SearchMediaItemController(
IIndexedEntitySearchService indexedEntitySearchService,
IMediaPresentationFactory mediaPresentationFactory,
IDataTypeService dataTypeService)
: this(
indexedEntitySearchService,
mediaPresentationFactory,
dataTypeService,
StaticServiceProvider.Instance.GetRequiredService<IMediaTypeService>())
{
}
[Obsolete("Use the non-obsolete constructor instead, will be removed in Umbraco 18.")]
@@ -77,6 +94,17 @@ public class SearchMediaItemController : MediaItemControllerBase
[FromQuery] IEnumerable<Guid>? allowedMediaTypes = null,
Guid? dataTypeId = null)
{
// We always want to include folders in the search results (aligns with behaviour in Umbraco 13, and allows folders
// to be selected to find the selectable items inside).
if (allowedMediaTypes is not null)
{
IMediaType? folderMediaType = _mediaTypeService.Get(Constants.Conventions.MediaTypes.Folder);
if (folderMediaType is not null && allowedMediaTypes.Contains(folderMediaType.Key) is false)
{
allowedMediaTypes = [..allowedMediaTypes, folderMediaType.Key];
}
}
var ignoreUserStartNodes = await IgnoreUserStartNodes(dataTypeId);
PagedModel<IEntitySlim> searchResult = await _indexedEntitySearchService.SearchAsync(
UmbracoObjectTypes.Media,
@@ -41,6 +41,7 @@ public class AllMigrationStatusPackageController : PackageControllerBase
/// <summary>
/// Gets a paginated list of the migration status of each installed package.
/// </summary>
/// <param name="cancellationToken">The cancellation token.</param>
/// <param name="skip">The amount of items to skip.</param>
/// <param name="take">The amount of items to take.</param>
/// <returns>The paged result of the installed packages migration status.</returns>
@@ -25,6 +25,7 @@ public class AllCreatedPackageController : CreatedPackageControllerBase
/// <summary>
/// Gets a paginated list of all created packages.
/// </summary>
/// <param name="cancellationToken">The cancellation token.</param>
/// <param name="skip">The amount of items to skip.</param>
/// <param name="take">The amount of items to take.</param>
/// <returns>The paged result of the created packages.</returns>
@@ -23,6 +23,7 @@ public class ByKeyCreatedPackageController : CreatedPackageControllerBase
/// <summary>
/// Gets a package by id.
/// </summary>
/// <param name="cancellationToken">The cancellation token.</param>
/// <param name="id">The id of the package.</param>
/// <returns>The package or not found result.</returns>
[HttpGet("{id:guid}")]
@@ -31,6 +31,7 @@ public class CreateCreatedPackageController : CreatedPackageControllerBase
/// <summary>
/// Creates a package.
/// </summary>
/// <param name="cancellationToken">The cancellation token.</param>
/// <param name="createPackageRequestModel">The model containing the data for a new package.</param>
/// <returns>The created package.</returns>
[HttpPost]
@@ -24,6 +24,7 @@ public class DeleteCreatedPackageController : CreatedPackageControllerBase
/// <summary>
/// Deletes a package with a given id.
/// </summary>
/// <param name="cancellationToken">The cancellation token.</param>
/// <param name="id">The id of the package.</param>
/// <returns>The result of the deletion.</returns>
[HttpDelete("{id:guid}")]
@@ -21,6 +21,7 @@ public class DownloadCreatedPackageController : CreatedPackageControllerBase
/// <summary>
/// Downloads a package XML or ZIP file.
/// </summary>
/// <param name="cancellationToken">The cancellation token.</param>
/// <param name="id">The id of the package.</param>
/// <returns>The XML or ZIP file of the package or not found result.</returns>
[HttpGet("{id:guid}/download")]
@@ -31,6 +31,7 @@ public class UpdateCreatedPackageController : CreatedPackageControllerBase
/// <summary>
/// Updates a package.
/// </summary>
/// <param name="cancellationToken">The cancellation token.</param>
/// <param name="id">The id of the package.</param>
/// <param name="updatePackageRequestModel">The model containing the data for updating a package.</param>
/// <returns>The created package.</returns>
@@ -18,6 +18,7 @@ public class RunMigrationPackageController : PackageControllerBase
/// <summary>
/// Runs all migration plans for a package with a given name if any are pending.
/// </summary>
/// <param name="cancellationToken">The cancellation token.</param>
/// <param name="name">The name of the package.</param>
/// <returns>The result of running the package migrations.</returns>
[HttpPost("{name}/run-migration")]
@@ -1,4 +1,4 @@
using Asp.Versioning;
using Asp.Versioning;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Core.Configuration;
using Umbraco.Cms.Core.Models.RedirectUrlManagement;
@@ -6,12 +6,20 @@ using Umbraco.Cms.Core.Security;
namespace Umbraco.Cms.Api.Management.Controllers.RedirectUrlManagement;
/// <summary>
/// Controller for setting the redirect URL tracking status.
/// </summary>
[ApiVersion("1.0")]
public class SetStatusRedirectUrlManagementController : RedirectUrlManagementControllerBase
{
private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor;
private readonly IConfigManipulator _configManipulator;
/// <summary>
/// Initializes a new instance of the <see cref="SetStatusRedirectUrlManagementController"/> class.
/// </summary>
/// <param name="backOfficeSecurityAccessor">The back office security accessor.</param>
/// <param name="configManipulator">The configuration manipulator.</param>
public SetStatusRedirectUrlManagementController(
IBackOfficeSecurityAccessor backOfficeSecurityAccessor,
IConfigManipulator configManipulator)
@@ -23,6 +31,12 @@ public class SetStatusRedirectUrlManagementController : RedirectUrlManagementCon
// TODO: Consider if we should even allow this, or only allow using the appsettings
// We generally don't want to edit the appsettings from our code.
// But maybe there is a valid use case for doing it on the fly.
/// <summary>
/// Sets the redirect URL tracking status.
/// </summary>
/// <param name="cancellationToken">The cancellation token for the HTTP request.</param>
/// <param name="status">The redirect status to set.</param>
/// <returns>An OK result if successful.</returns>
[HttpPost("status")]
[MapToApiVersion("1.0")]
public async Task<IActionResult> SetStatus(CancellationToken cancellationToken, [FromQuery] RedirectStatus status)
@@ -37,7 +51,8 @@ public class SetStatusRedirectUrlManagementController : RedirectUrlManagementCon
var enable = status switch
{
RedirectStatus.Enabled => true,
RedirectStatus.Disabled => false
RedirectStatus.Disabled => false,
_ => throw new ArgumentOutOfRangeException(nameof(status), status, "Unknown redirect status")
};
// For now I'm not gonna change this to limit breaking, but it's weird to have a "disabled" switch,
@@ -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;
@@ -33,6 +33,7 @@ public class FilterUserFilterController : UserFilterControllerBase
/// <summary>
/// Query users
/// </summary>
/// <param name="cancellationToken">The cancellation token.</param>
/// <param name="skip">Amount to skip.</param>
/// <param name="take">Amount to take.</param>
/// <param name="orderBy">Property to order by.</param>
@@ -60,15 +60,25 @@ internal static class BackOfficeAuthPolicyBuilderExtensions
AddAllowedApplicationsPolicy(AuthorizationPolicies.SectionAccessContentOrMedia, Constants.Applications.Content, Constants.Applications.Media);
AddAllowedApplicationsPolicy(
AuthorizationPolicies.SectionAccessForContentTree,
Constants.Applications.Content, Constants.Applications.Media, Constants.Applications.Users,
Constants.Applications.Settings, Constants.Applications.Packages, Constants.Applications.Members);
Constants.Applications.Content,
Constants.Applications.Media,
Constants.Applications.Users,
Constants.Applications.Settings,
Constants.Applications.Packages,
Constants.Applications.Members);
AddAllowedApplicationsPolicy(
AuthorizationPolicies.SectionAccessForMediaTree,
Constants.Applications.Content, Constants.Applications.Media, Constants.Applications.Users,
Constants.Applications.Settings, Constants.Applications.Packages, Constants.Applications.Members);
Constants.Applications.Content,
Constants.Applications.Media,
Constants.Applications.Users,
Constants.Applications.Settings,
Constants.Applications.Packages,
Constants.Applications.Members);
AddAllowedApplicationsPolicy(
AuthorizationPolicies.SectionAccessForMemberTree,
Constants.Applications.Content, Constants.Applications.Media, Constants.Applications.Members);
Constants.Applications.Content,
Constants.Applications.Media,
Constants.Applications.Members);
AddAllowedApplicationsPolicy(AuthorizationPolicies.SectionAccessMedia, Constants.Applications.Media);
AddAllowedApplicationsPolicy(AuthorizationPolicies.SectionAccessMembers, Constants.Applications.Members);
AddAllowedApplicationsPolicy(AuthorizationPolicies.SectionAccessPackages, Constants.Applications.Packages);
@@ -54,6 +54,7 @@ public class UserGroupPresentationFactory : IUserGroupPresentationFactory
{
Id = userGroup.Key,
Name = userGroup.Name ?? string.Empty,
Description = userGroup.Description ?? string.Empty,
Alias = userGroup.Alias,
DocumentStartNode = ReferenceByIdModel.ReferenceOrNull(contentStartNodeKey),
DocumentRootAccess = contentRootAccess,
@@ -87,6 +88,7 @@ public class UserGroupPresentationFactory : IUserGroupPresentationFactory
{
Id = userGroup.Key,
Name = userGroup.Name ?? string.Empty,
Description = userGroup.Description ?? string.Empty,
Alias = userGroup.Alias,
DocumentStartNode = ReferenceByIdModel.ReferenceOrNull(contentStartNodeKey),
MediaStartNode = ReferenceByIdModel.ReferenceOrNull(mediaStartNodeKey),
@@ -132,6 +134,7 @@ public class UserGroupPresentationFactory : IUserGroupPresentationFactory
{
Name = CleanUserGroupNameOrAliasForXss(requestModel.Name),
Alias = CleanUserGroupNameOrAliasForXss(requestModel.Alias),
Description = requestModel.Description,
Icon = requestModel.Icon,
HasAccessToAllLanguages = requestModel.HasAccessToAllLanguages,
Permissions = requestModel.FallbackPermissions,
@@ -197,9 +200,10 @@ public class UserGroupPresentationFactory : IUserGroupPresentationFactory
current.Name = CleanUserGroupNameOrAliasForXss(request.Name);
current.Alias = CleanUserGroupNameOrAliasForXss(request.Alias);
current.Description = request.Description;
current.Icon = request.Icon;
current.HasAccessToAllLanguages = request.HasAccessToAllLanguages;
current.Permissions = request.FallbackPermissions;
current.GranularPermissions = await _permissionPresentationFactory.CreatePermissionSetsAsync(request.Permissions);
@@ -1,4 +1,4 @@
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using Umbraco.Cms.Core;
@@ -19,7 +19,18 @@ public class PostUnattendedInstallNotificationHandler : INotificationAsyncHandle
private readonly IUserService _userService;
private readonly IMetricsConsentService _metricsConsentService;
public PostUnattendedInstallNotificationHandler(IOptions<UnattendedSettings> unattendedSettings, IUserService userService, IServiceScopeFactory serviceScopeFactory, IMetricsConsentService metricsConsentService)
/// <summary>
/// Initializes a new instance of the <see cref="PostUnattendedInstallNotificationHandler" /> class.
/// </summary>
/// <param name="unattendedSettings">The unattended settings.</param>
/// <param name="userService">The user service.</param>
/// <param name="serviceScopeFactory">The service scope factory.</param>
/// <param name="metricsConsentService">The metrics consent service.</param>
public PostUnattendedInstallNotificationHandler(
IOptions<UnattendedSettings> unattendedSettings,
IUserService userService,
IServiceScopeFactory serviceScopeFactory,
IMetricsConsentService metricsConsentService)
{
_unattendedSettings = unattendedSettings;
_userService = userService;
@@ -95,7 +106,9 @@ public class PostUnattendedInstallNotificationHandler : INotificationAsyncHandle
}
IdentityResult resetResult =
await backOfficeUserManager.ChangePasswordWithResetAsync(membershipUser.Id, resetToken,
await backOfficeUserManager.ChangePasswordWithResetAsync(
membershipUser.Id,
resetToken,
unattendedPassword!.Trim());
if (!resetResult.Succeeded)
{
@@ -1,4 +1,4 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Extensions;
using Microsoft.Extensions.Logging;
using Umbraco.Extensions;
@@ -30,7 +30,9 @@ public class UnhandledExceptionLoggerMiddleware : IMiddleware
}
catch (Exception e)
{
_logger.LogError(e, "Unhandled controller exception occurred for request '{RequestUrl}'",
_logger.LogError(
e,
"Unhandled controller exception occurred for request '{RequestUrl}'",
context.Request.GetEncodedPathAndQuery());
// Throw the error again, just in case it gets handled
throw;
@@ -1,6 +1,7 @@
using Microsoft.AspNetCore.Http;
using Microsoft.OpenApi;
using Swashbuckle.AspNetCore.SwaggerGen;
using Umbraco.Cms.Api.Management.DependencyInjection;
using Umbraco.Cms.Api.Management.ViewModels;
using Umbraco.Cms.Core;
@@ -10,6 +11,12 @@ internal sealed class NotificationHeaderFilter : IOperationFilter
{
public void Apply(OpenApiOperation operation, OperationFilterContext context)
{
// Only apply to the Umbraco CMS Management API.
if (context.DocumentName != ManagementApiConfiguration.ApiName)
{
return;
}
if (context.ApiDescription.HttpMethod == HttpMethod.Get.Method)
{
return;
@@ -52,7 +52,9 @@ public abstract class MustSatisfyRequirementAuthorizationHandler<T, TResource> :
where T : IAuthorizationRequirement
{
/// <inheritdoc />
protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, T requirement,
protected override async Task HandleRequirementAsync(
AuthorizationHandlerContext context,
T requirement,
TResource resource)
{
var isAuth = await IsAuthorized(context, requirement, resource);
@@ -7,5 +7,13 @@ namespace Umbraco.Cms.Api.Management.Security;
/// </summary>
public class ExternalLoginSignInResult : SignInResult
{
public static ExternalLoginSignInResult NotAllowed { get; } = new() { Succeeded = false };
/// <summary>
/// Gets a <see cref="ExternalLoginSignInResult" /> that represents a sign-in attempt that failed because
/// the user is not allowed to sign-in.
/// </summary>
/// <value>
/// A <see cref="ExternalLoginSignInResult" /> that represents a sign-in attempt that failed because
/// the user is not allowed to sign-in.
/// </value>
public static new ExternalLoginSignInResult NotAllowed { get; } = new() { Succeeded = false };
}
@@ -1,13 +1,19 @@
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Events;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.Entities;
using Umbraco.Cms.Core.Models.Membership;
using Umbraco.Cms.Core.Models.ServerEvents;
using Umbraco.Cms.Core.Notifications;
using Umbraco.Cms.Core.ServerEvents;
using Umbraco.Cms.Core.Services;
namespace Umbraco.Cms.Api.Management.ServerEvents;
/// <summary>
/// Handles server event notifications for various entity types by routing create, update, delete, and recycle bin
/// events to connected clients or systems.
/// </summary>
internal sealed class ServerEventSender :
INotificationAsyncHandler<ContentSavedNotification>,
INotificationAsyncHandler<ContentSavedBlueprintNotification>,
@@ -57,8 +63,16 @@ internal sealed class ServerEventSender :
INotificationAsyncHandler<MediaMovedToRecycleBinNotification>
{
private readonly IServerEventRouter _serverEventRouter;
private readonly IIdKeyMap _idKeyMap;
public ServerEventSender(IServerEventRouter serverEventRouter) => _serverEventRouter = serverEventRouter;
/// <summary>
/// Initializes a new instance of the <see cref="ServerEventSender"/> class.
/// </summary>
public ServerEventSender(IServerEventRouter serverEventRouter, IIdKeyMap idKeyMap)
{
_serverEventRouter = serverEventRouter;
_idKeyMap = idKeyMap;
}
private async Task NotifySavedAsync<T>(SavedNotification<T> notification, string source)
where T : IEntity
@@ -113,66 +127,115 @@ internal sealed class ServerEventSender :
}
}
/// <inheritdoc/>
public async Task HandleAsync(ContentSavedNotification notification, CancellationToken cancellationToken) =>
await NotifySavedAsync(notification, Constants.ServerEvents.EventSource.Document);
/// <inheritdoc/>
public async Task HandleAsync(ContentSavedBlueprintNotification notification, CancellationToken cancellationToken) =>
await RouteCreatedOrUpdatedEvent(Constants.ServerEvents.EventSource.DocumentBlueprint, notification.SavedBlueprint);
/// <inheritdoc/>
public async Task HandleAsync(ContentTypeSavedNotification notification, CancellationToken cancellationToken) =>
await NotifySavedAsync(notification, Constants.ServerEvents.EventSource.DocumentType);
/// <inheritdoc/>
public async Task HandleAsync(MediaSavedNotification notification, CancellationToken cancellationToken)
=> await NotifySavedAsync(notification, Constants.ServerEvents.EventSource.Media);
/// <inheritdoc/>
public async Task HandleAsync(MediaTypeSavedNotification notification, CancellationToken cancellationToken) =>
await NotifySavedAsync(notification, Constants.ServerEvents.EventSource.MediaType);
/// <inheritdoc/>
public async Task HandleAsync(MemberSavedNotification notification, CancellationToken cancellationToken) =>
await NotifySavedAsync(notification, Constants.ServerEvents.EventSource.Member);
/// <inheritdoc/>
public async Task HandleAsync(MemberTypeSavedNotification notification, CancellationToken cancellationToken) =>
await NotifySavedAsync(notification, Constants.ServerEvents.EventSource.MemberType);
/// <inheritdoc/>
public async Task HandleAsync(MemberGroupSavedNotification notification, CancellationToken cancellationToken) =>
await NotifySavedAsync(notification, Constants.ServerEvents.EventSource.MemberGroup);
/// <inheritdoc/>
public async Task HandleAsync(DataTypeSavedNotification notification, CancellationToken cancellationToken) =>
await NotifySavedAsync(notification, Constants.ServerEvents.EventSource.DataType);
/// <inheritdoc/>
public async Task HandleAsync(LanguageSavedNotification notification, CancellationToken cancellationToken) =>
await NotifySavedAsync(notification, Constants.ServerEvents.EventSource.Language);
/// <inheritdoc/>
public async Task HandleAsync(ScriptSavedNotification notification, CancellationToken cancellationToken) =>
await NotifySavedAsync(notification, Constants.ServerEvents.EventSource.Script);
/// <inheritdoc/>
public async Task HandleAsync(StylesheetSavedNotification notification, CancellationToken cancellationToken) =>
await NotifySavedAsync(notification, Constants.ServerEvents.EventSource.Stylesheet);
/// <inheritdoc/>
public async Task HandleAsync(TemplateSavedNotification notification, CancellationToken cancellationToken) =>
await NotifySavedAsync(notification, Constants.ServerEvents.EventSource.Template);
/// <inheritdoc/>
public async Task HandleAsync(DictionaryItemSavedNotification notification, CancellationToken cancellationToken) =>
await NotifySavedAsync(notification, Constants.ServerEvents.EventSource.DictionaryItem);
/// <inheritdoc/>
public async Task HandleAsync(DomainSavedNotification notification, CancellationToken cancellationToken) =>
await NotifySavedAsync(notification, Constants.ServerEvents.EventSource.Domain);
/// <inheritdoc/>
public async Task HandleAsync(PartialViewSavedNotification notification, CancellationToken cancellationToken) =>
await NotifySavedAsync(notification, Constants.ServerEvents.EventSource.PartialView);
public async Task HandleAsync(PublicAccessEntrySavedNotification notification, CancellationToken cancellationToken) =>
/// <inheritdoc/>
public async Task HandleAsync(PublicAccessEntrySavedNotification notification, CancellationToken cancellationToken)
{
await NotifySavedAsync(notification, Constants.ServerEvents.EventSource.PublicAccessEntry);
// For public access entries, we also need to notify affected content items, so any client-side
// cache for the document can be invalidated.
await RouteDocumentUpdatedEventForPublicAccessModification(notification.SavedEntities);
}
private async Task RouteDocumentUpdatedEventForPublicAccessModification(IEnumerable<PublicAccessEntry> entities)
{
foreach (PublicAccessEntry entity in entities)
{
Attempt<Guid> getKeyAttempt = _idKeyMap.GetKeyForId(
entity.ProtectedNodeId,
UmbracoObjectTypes.Document);
if (getKeyAttempt.Success is false)
{
continue;
}
var eventModel = new ServerEvent
{
EventType = Constants.ServerEvents.EventType.Updated,
Key = getKeyAttempt.Result,
EventSource = Constants.ServerEvents.EventSource.Document,
};
await _serverEventRouter.RouteEventAsync(eventModel);
}
}
/// <inheritdoc/>
public async Task HandleAsync(RelationSavedNotification notification, CancellationToken cancellationToken) =>
await NotifySavedAsync(notification, Constants.ServerEvents.EventSource.Relation);
/// <inheritdoc/>
public async Task HandleAsync(RelationTypeSavedNotification notification, CancellationToken cancellationToken) =>
await NotifySavedAsync(notification, Constants.ServerEvents.EventSource.RelationType);
/// <inheritdoc/>
public async Task HandleAsync(UserGroupSavedNotification notification, CancellationToken cancellationToken) =>
await NotifySavedAsync(notification, Constants.ServerEvents.EventSource.UserGroup);
/// <inheritdoc/>
public async Task HandleAsync(UserSavedNotification notification, CancellationToken cancellationToken)
{
// We still need to notify of saved entities like any other event source.
@@ -192,12 +255,15 @@ internal sealed class ServerEventSender :
}
}
/// <inheritdoc/>
public async Task HandleAsync(WebhookSavedNotification notification, CancellationToken cancellationToken) =>
await NotifySavedAsync(notification, Constants.ServerEvents.EventSource.Webhook);
/// <inheritdoc/>
public async Task HandleAsync(ContentDeletedNotification notification, CancellationToken cancellationToken) =>
await NotifyDeletedAsync(notification, Constants.ServerEvents.EventSource.Document);
/// <inheritdoc/>
public async Task HandleAsync(ContentDeletedBlueprintNotification notification, CancellationToken cancellationToken)
{
foreach (Core.Models.IContent entity in notification.DeletedBlueprints)
@@ -206,69 +272,97 @@ internal sealed class ServerEventSender :
}
}
/// <inheritdoc/>
public async Task HandleAsync(ContentTypeDeletedNotification notification, CancellationToken cancellationToken) =>
await NotifyDeletedAsync(notification, Constants.ServerEvents.EventSource.DocumentType);
/// <inheritdoc/>
public async Task HandleAsync(MediaDeletedNotification notification, CancellationToken cancellationToken) =>
await NotifyDeletedAsync(notification, Constants.ServerEvents.EventSource.Media);
/// <inheritdoc/>
public async Task HandleAsync(MediaTypeDeletedNotification notification, CancellationToken cancellationToken) =>
await NotifyDeletedAsync(notification, Constants.ServerEvents.EventSource.MediaType);
/// <inheritdoc/>
public async Task HandleAsync(MemberDeletedNotification notification, CancellationToken cancellationToken) =>
await NotifyDeletedAsync(notification, Constants.ServerEvents.EventSource.Member);
/// <inheritdoc/>
public async Task HandleAsync(MemberTypeDeletedNotification notification, CancellationToken cancellationToken) =>
await NotifyDeletedAsync(notification, Constants.ServerEvents.EventSource.MemberType);
/// <inheritdoc/>
public async Task HandleAsync(MemberGroupDeletedNotification notification, CancellationToken cancellationToken) =>
await NotifyDeletedAsync(notification, Constants.ServerEvents.EventSource.MemberGroup);
/// <inheritdoc/>
public async Task HandleAsync(DataTypeDeletedNotification notification, CancellationToken cancellationToken) =>
await NotifyDeletedAsync(notification, Constants.ServerEvents.EventSource.DataType);
/// <inheritdoc/>
public async Task HandleAsync(LanguageDeletedNotification notification, CancellationToken cancellationToken) =>
await NotifyDeletedAsync(notification, Constants.ServerEvents.EventSource.Language);
/// <inheritdoc/>
public async Task HandleAsync(ScriptDeletedNotification notification, CancellationToken cancellationToken) =>
await NotifyDeletedAsync(notification, Constants.ServerEvents.EventSource.Script);
/// <inheritdoc/>
public async Task HandleAsync(StylesheetDeletedNotification notification, CancellationToken cancellationToken) =>
await NotifyDeletedAsync(notification, Constants.ServerEvents.EventSource.Stylesheet);
/// <inheritdoc/>
public async Task HandleAsync(TemplateDeletedNotification notification, CancellationToken cancellationToken) =>
await NotifyDeletedAsync(notification, Constants.ServerEvents.EventSource.Template);
/// <inheritdoc/>
public async Task HandleAsync(DictionaryItemDeletedNotification notification, CancellationToken cancellationToken) =>
await NotifyDeletedAsync(notification, Constants.ServerEvents.EventSource.DictionaryItem);
/// <inheritdoc/>
public async Task HandleAsync(DomainDeletedNotification notification, CancellationToken cancellationToken) =>
await NotifyDeletedAsync(notification, Constants.ServerEvents.EventSource.Domain);
/// <inheritdoc/>
public async Task HandleAsync(PartialViewDeletedNotification notification, CancellationToken cancellationToken) =>
await NotifyDeletedAsync(notification, Constants.ServerEvents.EventSource.PartialView);
public async Task HandleAsync(PublicAccessEntryDeletedNotification notification, CancellationToken cancellationToken) =>
/// <inheritdoc/>
public async Task HandleAsync(PublicAccessEntryDeletedNotification notification, CancellationToken cancellationToken)
{
await NotifyDeletedAsync(notification, Constants.ServerEvents.EventSource.PublicAccessEntry);
// For public access entries, we also need to notify affected content items, so any client-side
// cache for the document can be invalidated.
await RouteDocumentUpdatedEventForPublicAccessModification(notification.DeletedEntities);
}
/// <inheritdoc/>
public async Task HandleAsync(RelationDeletedNotification notification, CancellationToken cancellationToken) =>
await NotifyDeletedAsync(notification, Constants.ServerEvents.EventSource.Relation);
/// <inheritdoc/>
public async Task HandleAsync(RelationTypeDeletedNotification notification, CancellationToken cancellationToken) =>
await NotifyDeletedAsync(notification, Constants.ServerEvents.EventSource.RelationType);
/// <inheritdoc/>
public async Task HandleAsync(UserGroupDeletedNotification notification, CancellationToken cancellationToken) =>
await NotifyDeletedAsync(notification, Constants.ServerEvents.EventSource.UserGroup);
/// <inheritdoc/>
public async Task HandleAsync(UserDeletedNotification notification, CancellationToken cancellationToken) =>
await NotifyDeletedAsync(notification, Constants.ServerEvents.EventSource.User);
/// <inheritdoc/>
public async Task HandleAsync(WebhookDeletedNotification notification, CancellationToken cancellationToken) =>
await NotifyDeletedAsync(notification, Constants.ServerEvents.EventSource.Webhook);
/// <inheritdoc/>
public async Task HandleAsync(ContentMovedToRecycleBinNotification notification, CancellationToken cancellationToken) =>
await NotifyTrashedAsync(notification, Constants.ServerEvents.EventSource.Document);
/// <inheritdoc/>
public async Task HandleAsync(MediaMovedToRecycleBinNotification notification, CancellationToken cancellationToken) =>
await NotifyTrashedAsync(notification, Constants.ServerEvents.EventSource.Media);
}
@@ -11,12 +11,21 @@ using Umbraco.Extensions;
namespace Umbraco.Cms.Api.Management.Services.Entities;
/// <summary>
/// Provides functionality for retrieving user start node entities with access information.
/// </summary>
public class UserStartNodeEntitiesService : IUserStartNodeEntitiesService
{
private readonly IEntityService _entityService;
private readonly ICoreScopeProvider _scopeProvider;
private readonly IIdKeyMap _idKeyMap;
/// <summary>
/// Initializes a new instance of the <see cref="UserStartNodeEntitiesService"/> class.
/// </summary>
/// <param name="entityService">The entity service.</param>
/// <param name="scopeProvider">The core scope provider.</param>
/// <param name="idKeyMap">The ID to key mapping service.</param>
public UserStartNodeEntitiesService(IEntityService entityService, ICoreScopeProvider scopeProvider, IIdKeyMap idKeyMap)
{
_entityService = entityService;
@@ -150,8 +159,7 @@ public class UserStartNodeEntitiesService : IUserStartNodeEntitiesService
int after,
Ordering ordering,
out long totalBefore,
out long totalAfter
)
out long totalAfter)
{
Attempt<int> targetIdAttempt = _idKeyMap.GetIdForKey(targetKey, umbracoObjectType);
if (targetIdAttempt.Success is false)
@@ -2,12 +2,19 @@
namespace Umbraco.Cms.Api.Management.ViewModels.MediaType;
/// <summary>
/// Represents a request model for creating a media type.
/// </summary>
public class CreateMediaTypeRequestModel
: CreateContentTypeWithParentRequestModelBase<CreateMediaTypePropertyTypeRequestModel, CreateMediaTypePropertyTypeContainerRequestModel>
{
/// <summary>
/// Gets or sets the allowed media types that can be created under this media type.
/// </summary>
public IEnumerable<MediaTypeSort> AllowedMediaTypes { get; set; } = Enumerable.Empty<MediaTypeSort>();
/// <summary>
/// Gets or sets the compositions for this media type.
/// </summary>
public IEnumerable<MediaTypeComposition> Compositions { get; set; } = Enumerable.Empty<MediaTypeComposition>();
public ReferenceByIdModel? Collection { get; set; }
}
@@ -1,4 +1,4 @@
using Umbraco.Cms.Api.Management.ViewModels.UserGroup.Permissions;
using Umbraco.Cms.Api.Management.ViewModels.UserGroup.Permissions;
namespace Umbraco.Cms.Api.Management.ViewModels.UserGroup;
@@ -22,6 +22,11 @@ public class UserGroupBase
/// </summary>
public required string Alias { get; init; }
/// <summary>
/// The description of the user group
/// </summary>
public string? Description { get; set; }
/// <summary>
/// The Icon for the user group
/// </summary>
@@ -7,6 +7,8 @@ using Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation;
using Microsoft.CodeAnalysis;
using Microsoft.Extensions.Options;
#pragma warning disable ASPDEPR003 // MvcRazorRuntimeCompilationOptions is obsolete - intentional use for development-mode runtime compilation
namespace Umbraco.Cms.DevelopmentMode.Backoffice.InMemoryAuto;
@@ -7,6 +7,8 @@ using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Umbraco.Cms.Core.Exceptions;
#pragma warning disable ASPDEPR003 // MvcRazorRuntimeCompilationOptions is obsolete - intentional use for development-mode runtime compilation
namespace Umbraco.Cms.DevelopmentMode.Backoffice.InMemoryAuto;
internal sealed class UmbracoViewCompilerProvider : IViewCompilerProvider
@@ -6,6 +6,10 @@ using Size = System.Drawing.Size;
namespace Umbraco.Cms.Imaging.ImageSharp.Media;
/// <summary>
/// Extracts image dimensions using ImageSharp, with support for EXIF orientation.
/// </summary>
/// <seealso cref="IImageDimensionExtractor" />
public sealed class ImageSharpDimensionExtractor : IImageDimensionExtractor
{
private readonly Configuration _configuration;
@@ -29,7 +29,7 @@ public sealed class ImageSharpImageUrlGenerator : IImageUrlGenerator
/// </summary>
/// <param name="configuration">The ImageSharp configuration.</param>
/// <param name="requestAuthorizationUtilities">Contains helpers that allow authorization of image requests.</param>
/// <param name="options"></param>
/// <param name="options">The ImageSharp middleware options.</param>
public ImageSharpImageUrlGenerator(
Configuration configuration,
RequestAuthorizationUtilities? requestAuthorizationUtilities,
@@ -14,11 +14,16 @@ using Umbraco.Extensions;
namespace Umbraco.Cms.Imaging.ImageSharp;
/// <summary>
/// Extension methods for <see cref="IUmbracoBuilder" /> to add ImageSharp image processing.
/// </summary>
public static class UmbracoBuilderExtensions
{
/// <summary>
/// Adds Image Sharp with Umbraco settings
/// Adds ImageSharp image processing with Umbraco settings.
/// </summary>
/// <param name="builder">The <see cref="IUmbracoBuilder" />.</param>
/// <returns>The <see cref="IServiceCollection" />.</returns>
public static IServiceCollection AddUmbracoImageSharp(this IUmbracoBuilder builder)
{
// Add default ImageSharp configuration and service implementations
@@ -5,6 +5,10 @@ using Size = System.Drawing.Size;
namespace Umbraco.Cms.Imaging.ImageSharp.Media;
/// <summary>
/// Extracts image dimensions using ImageSharp, with support for EXIF orientation.
/// </summary>
/// <seealso cref="IImageDimensionExtractor" />
public sealed class ImageSharpDimensionExtractor : IImageDimensionExtractor
{
private readonly Configuration _configuration;
@@ -14,11 +14,16 @@ using Umbraco.Extensions;
namespace Umbraco.Cms.Imaging.ImageSharp;
/// <summary>
/// Extension methods for <see cref="IUmbracoBuilder" /> to add ImageSharp 2.x image processing.
/// </summary>
public static class UmbracoBuilderExtensions
{
/// <summary>
/// Adds Image Sharp with Umbraco settings
/// Adds ImageSharp with Umbraco settings.
/// </summary>
/// <param name="builder">The Umbraco builder.</param>
/// <returns>The <see cref="IServiceCollection" />.</returns>
public static IServiceCollection AddUmbracoImageSharp(this IUmbracoBuilder builder)
{
// Add default ImageSharp configuration and service implementations
@@ -4,7 +4,7 @@ namespace Umbraco.Cms.Persistence.EFCore.Scoping;
internal interface IAmbientEFCoreScopeStack<TDbContext> : IEFCoreScopeAccessor<TDbContext> where TDbContext : DbContext
{
public IEfCoreScope<TDbContext>? AmbientScope { get; }
new IEfCoreScope<TDbContext>? AmbientScope { get; }
IEfCoreScope<TDbContext> Pop();
@@ -13,7 +13,10 @@ public interface IEfCoreScope<TDbContext> : ICoreScope
/// <returns></returns>
Task<T> ExecuteWithContextAsync<T>(Func<TDbContext, Task<T>> method);
public IScopeContext? ScopeContext { get; set; }
/// <summary>
/// Gets or sets the scope context.
/// </summary>
IScopeContext? ScopeContext { get; set; }
/// <summary>
/// Executes the given function on the database.
@@ -26,5 +29,5 @@ public interface IEfCoreScope<TDbContext> : ICoreScope
/// <summary>
/// Gets the scope notification publisher
/// </summary>
IScopedNotificationPublisher Notifications { get; }
new IScopedNotificationPublisher Notifications { get; }
}
@@ -11,6 +11,17 @@ using Umbraco.Extensions;
namespace Umbraco.Cms.Core.Cache;
/// <summary>
/// Provides cache refresh functionality for content items, ensuring that content-related caches are updated or
/// invalidated in response to content changes.
/// </summary>
/// <remarks>
/// The ContentCacheRefresher coordinates cache invalidation for content, including memory caches, URL
/// caches, navigation structures, and domain assignments. It responds to content change notifications and ensures that
/// all relevant caches reflect the current state of published and unpublished content. This refresher is used
/// internally to maintain cache consistency after content operations such as publish, unpublish, move,
/// or delete.
/// </remarks>
public sealed class ContentCacheRefresher : PayloadCacheRefresherBase<ContentCacheRefresherNotification,
ContentCacheRefresher.JsonPayload>
{
@@ -25,6 +36,9 @@ public sealed class ContentCacheRefresher : PayloadCacheRefresherBase<ContentCac
private readonly IPublishStatusManagementService _publishStatusManagementService;
private readonly IIdKeyMap _idKeyMap;
/// <summary>
/// Initializes a new instance of the <see cref="ContentCacheRefresher"/> class.
/// </summary>
public ContentCacheRefresher(
AppCaches appCaches,
IJsonSerializer serializer,
@@ -60,11 +74,15 @@ public sealed class ContentCacheRefresher : PayloadCacheRefresherBase<ContentCac
#region Indirect
/// <summary>
/// Clears cached content and public access data from the provided application caches.
/// </summary>
/// <param name="appCaches">The application caches instance from which to clear content and public access entries.</param>
public static void RefreshContentTypes(AppCaches appCaches)
{
// we could try to have a mechanism to notify the PublishedCachesService
// We could try to have a mechanism to notify the PublishedCachesService
// and figure out whether published items were modified or not... keep it
// simple for now, just clear the whole thing
// simple for now, just clear the whole thing.
appCaches.ClearPartialViewCache();
appCaches.IsolatedCaches.ClearCache<PublicAccessEntry>();
@@ -75,16 +93,22 @@ public sealed class ContentCacheRefresher : PayloadCacheRefresherBase<ContentCac
#region Define
/// <summary>
/// Represents a unique identifier for the cache refresher.
/// </summary>
public static readonly Guid UniqueId = Guid.Parse("900A4FBE-DF3C-41E6-BB77-BE896CD158EA");
/// <inheritdoc/>
public override Guid RefresherUniqueId => UniqueId;
/// <inheritdoc/>
public override string Name => "ContentCacheRefresher";
#endregion
#region Refresher
/// <inheritdoc/>
public override void RefreshInternal(JsonPayload[] payloads)
{
AppCaches.RuntimeCache.ClearOfType<PublicAccessEntry>();
@@ -121,13 +145,15 @@ public sealed class ContentCacheRefresher : PayloadCacheRefresherBase<ContentCac
base.RefreshInternal(payloads);
}
/// <inheritdoc/>
public override void Refresh(JsonPayload[] payloads)
{
var idsRemoved = new HashSet<int>();
foreach (JsonPayload payload in payloads)
{
// if the item is not a blueprint and is being completely removed, we need to refresh the domains cache if any domain was assigned to the content
// If the item is not a blueprint and is being completely removed, we need to refresh the domains cache if any domain was assigned to the content.
// So track the IDs that have been removed.
if (payload.Blueprint is false && payload.ChangeTypes.HasTypesAny(TreeChangeTypes.Remove))
{
idsRemoved.Add(payload.Id);
@@ -138,43 +164,18 @@ public sealed class ContentCacheRefresher : PayloadCacheRefresherBase<ContentCac
HandleNavigation(payload);
HandlePublishedAsync(payload, CancellationToken.None).GetAwaiter().GetResult();
if (payload.Id != default)
{
_idKeyMap.ClearCache(payload.Id);
}
if (payload.Key.HasValue)
{
_idKeyMap.ClearCache(payload.Key.Value);
}
HandleIdKeyMap(payload);
}
// Clear partial view cache when published content changes
// Clear partial view cache when published content changes.
if (ShouldClearPartialViewCache(payloads))
{
AppCaches.ClearPartialViewCache();
}
if (idsRemoved.Count > 0)
{
var assignedDomains = _domainService.GetAll(true)
?.Where(x => x.RootContentId.HasValue && idsRemoved.Contains(x.RootContentId.Value)).ToList();
if (assignedDomains?.Count > 0)
{
// TODO: this is duplicating the logic in DomainCacheRefresher BUT we cannot inject that into this because it it not registered explicitly in the container,
// and we cannot inject the CacheRefresherCollection since that would be a circular reference, so what is the best way to call directly in to the
// DomainCacheRefresher?
ClearAllIsolatedCacheByEntityType<IDomain>();
// note: must do what's above FIRST else the repositories still have the old cached
// content and when the PublishedCachesService is notified of changes it does not see
// the new content...
// notify
_domainCacheService.Refresh(assignedDomains
.Select(x => new DomainCacheRefresher.JsonPayload(x.Id, DomainChangeTypes.Remove)).ToArray());
}
}
// Clear the domain cache if any domain is assigned to removed content.
HandleDomainCache(idsRemoved);
base.Refresh(payloads);
}
@@ -251,18 +252,51 @@ public sealed class ContentCacheRefresher : PayloadCacheRefresherBase<ContentCac
}
}
private bool IsBranchUnpublished(JsonPayload payload)
{
private static bool IsBranchUnpublished(JsonPayload payload) =>
// If unpublished cultures has one or more values, but published cultures does not, this means that the branch is unpublished entirely
// And therefore should no longer be resolve-able from the cache, so we need to remove it instead.
// Otherwise, some culture is still published, so it should be resolve-able from cache, and published cultures should instead be used.
return payload.UnpublishedCultures is not null && payload.UnpublishedCultures.Length != 0 &&
payload.UnpublishedCultures is not null && payload.UnpublishedCultures.Length != 0 &&
(payload.PublishedCultures is null || payload.PublishedCultures.Length == 0);
private void HandleRouting(JsonPayload payload)
{
if (payload.ChangeTypes.HasType(TreeChangeTypes.Remove))
{
Guid key = payload.Key ?? _idKeyMap.GetKeyForId(payload.Id, UmbracoObjectTypes.Document).Result;
// Note that we need to clear the navigation service as the last thing.
if (_documentNavigationQueryService.TryGetDescendantsKeysOrSelfKeys(key, out IEnumerable<Guid>? descendantsOrSelfKeys))
{
_documentUrlService.DeleteUrlsFromCacheAsync(descendantsOrSelfKeys).GetAwaiter().GetResult();
}
else if (_documentNavigationQueryService.TryGetDescendantsKeysOrSelfKeysInBin(key, out IEnumerable<Guid>? descendantsOrSelfKeysInBin))
{
_documentUrlService.DeleteUrlsFromCacheAsync(descendantsOrSelfKeysInBin).GetAwaiter().GetResult();
}
}
if (payload.ChangeTypes.HasType(TreeChangeTypes.RefreshAll))
{
_documentUrlService.InitAsync(false, CancellationToken.None).GetAwaiter().GetResult(); // TODO: make async
}
if (payload.ChangeTypes.HasType(TreeChangeTypes.RefreshNode))
{
Guid key = payload.Key ?? _idKeyMap.GetKeyForId(payload.Id, UmbracoObjectTypes.Document).Result;
_documentUrlService.CreateOrUpdateUrlSegmentsAsync(key).GetAwaiter().GetResult();
}
if (payload.ChangeTypes.HasType(TreeChangeTypes.RefreshBranch))
{
Guid key = payload.Key ?? _idKeyMap.GetKeyForId(payload.Id, UmbracoObjectTypes.Document).Result;
_documentUrlService.CreateOrUpdateUrlSegmentsWithDescendantsAsync(key).GetAwaiter().GetResult();
}
}
private void HandleNavigation(JsonPayload payload)
{
if (payload.ChangeTypes.HasType(TreeChangeTypes.RefreshAll))
{
_documentNavigationManagementService.RebuildAsync().GetAwaiter().GetResult();
@@ -389,50 +423,68 @@ public sealed class ContentCacheRefresher : PayloadCacheRefresherBase<ContentCac
await _publishStatusManagementService.AddOrUpdateStatusWithDescendantsAsync(payload.Key.Value, cancellationToken);
}
}
private void HandleRouting(JsonPayload payload)
private void HandleIdKeyMap(JsonPayload payload)
{
if (payload.ChangeTypes.HasType(TreeChangeTypes.Remove))
// We only need to flush the ID/Key map when content is deleted.
if (payload.ChangeTypes.HasTypesAny(TreeChangeTypes.Remove) is false)
{
var key = payload.Key ?? _idKeyMap.GetKeyForId(payload.Id, UmbracoObjectTypes.Document).Result;
//Note the we need to clear the navigation service as the last thing
if (_documentNavigationQueryService.TryGetDescendantsKeysOrSelfKeys(key, out var descendantsOrSelfKeys))
{
_documentUrlService.DeleteUrlsFromCacheAsync(descendantsOrSelfKeys).GetAwaiter().GetResult();
}
else if (_documentNavigationQueryService.TryGetDescendantsKeysOrSelfKeysInBin(key, out var descendantsOrSelfKeysInBin))
{
_documentUrlService.DeleteUrlsFromCacheAsync(descendantsOrSelfKeysInBin).GetAwaiter().GetResult();
}
}
if (payload.ChangeTypes.HasType(TreeChangeTypes.RefreshAll))
{
_documentUrlService.InitAsync(false, CancellationToken.None).GetAwaiter().GetResult(); //TODO make async
return;
}
if (payload.ChangeTypes.HasType(TreeChangeTypes.RefreshNode))
if (payload.Id != default)
{
var key = payload.Key ?? _idKeyMap.GetKeyForId(payload.Id, UmbracoObjectTypes.Document).Result;
_documentUrlService.CreateOrUpdateUrlSegmentsAsync(key).GetAwaiter().GetResult();
_idKeyMap.ClearCache(payload.Id);
}
if (payload.ChangeTypes.HasType(TreeChangeTypes.RefreshBranch))
if (payload.Key.HasValue)
{
var key = payload.Key ?? _idKeyMap.GetKeyForId(payload.Id, UmbracoObjectTypes.Document).Result;
_documentUrlService.CreateOrUpdateUrlSegmentsWithDescendantsAsync(key).GetAwaiter().GetResult();
_idKeyMap.ClearCache(payload.Key.Value);
}
}
// these events should never trigger
// everything should be PAYLOAD/JSON
private void HandleDomainCache(HashSet<int> idsRemoved)
{
if (idsRemoved.Count == 0)
{
return;
}
#pragma warning disable CS0618 // Type or member is obsolete
var assignedDomains = _domainService.GetAll(true)
.Where(x => x.RootContentId.HasValue && idsRemoved.Contains(x.RootContentId.Value))
.ToList();
#pragma warning restore CS0618 // Type or member is obsolete
if (assignedDomains.Count <= 0)
{
return;
}
// TODO: this is duplicating the logic in DomainCacheRefresher BUT we cannot inject that into this because it it not registered explicitly in the container,
// and we cannot inject the CacheRefresherCollection since that would be a circular reference, so what is the best way to call directly in to the
// DomainCacheRefresher?
ClearAllIsolatedCacheByEntityType<IDomain>();
// note: must do what's above FIRST else the repositories still have the old cached
// content and when the PublishedCachesService is notified of changes it does not see
// the new content...
// notify
_domainCacheService.Refresh(assignedDomains
.Select(x => new DomainCacheRefresher.JsonPayload(x.Id, DomainChangeTypes.Remove)).ToArray());
}
// These events should never trigger. Everything should be PAYLOAD/JSON.
/// <inheritdoc/>
public override void RefreshAll() => throw new NotSupportedException();
/// <inheritdoc/>
public override void Refresh(int id) => throw new NotSupportedException();
/// <inheritdoc/>
public override void Refresh(Guid id) => throw new NotSupportedException();
/// <inheritdoc/>
public override void Remove(int id) => throw new NotSupportedException();
#endregion
@@ -440,19 +492,40 @@ public sealed class ContentCacheRefresher : PayloadCacheRefresherBase<ContentCac
#region Json
// TODO (V14): Change into a record
/// <summary>
/// Represents a JSON-serializable payload containing information about a content or tree change event, including
/// identifiers, change types, and culture-specific publishing details.
/// </summary>
public class JsonPayload
{
/// <summary>
/// Gets the unique integer identifier for the entity.
/// </summary>
public int Id { get; init; }
/// <summary>
/// Gets the unique GUID key associated with the entity, or null if no key is assigned.
/// </summary>
public Guid? Key { get; init; }
/// <summary>
/// Gets the types of changes that have occurred in the tree.
/// </summary>
public TreeChangeTypes ChangeTypes { get; init; }
/// <summary>
/// Gets a value indicating whether the content represents a document blueprint.
/// </summary>
public bool Blueprint { get; init; }
/// <summary>
/// Gets the collection of culture codes in which the content is published.
/// </summary>
public string[]? PublishedCultures { get; init; }
/// <summary>
/// Gets the collection of culture codes for which the content has been unpublished.
/// </summary>
public string[]? UnpublishedCultures { get; init; }
}
@@ -1,3 +1,5 @@
using Microsoft.Extensions.DependencyInjection;
using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Core.Events;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.PublishedContent;
@@ -18,6 +20,7 @@ public sealed class DataTypeCacheRefresher : PayloadCacheRefresherBase<DataTypeC
private readonly IPublishedContentTypeCache _publishedContentTypeCache;
private readonly IDocumentCacheService _documentCacheService;
private readonly IMediaCacheService _mediaCacheService;
private readonly IContentTypeCommonRepository _contentTypeCommonRepository;
public DataTypeCacheRefresher(
AppCaches appCaches,
@@ -29,7 +32,8 @@ public sealed class DataTypeCacheRefresher : PayloadCacheRefresherBase<DataTypeC
IPublishedContentTypeFactory publishedContentTypeFactory,
IPublishedContentTypeCache publishedContentTypeCache,
IDocumentCacheService documentCacheService,
IMediaCacheService mediaCacheService)
IMediaCacheService mediaCacheService,
IContentTypeCommonRepository contentTypeCommonRepository)
: base(appCaches, serializer, eventAggregator, factory)
{
_idKeyMap = idKeyMap;
@@ -38,6 +42,34 @@ public sealed class DataTypeCacheRefresher : PayloadCacheRefresherBase<DataTypeC
_publishedContentTypeCache = publishedContentTypeCache;
_documentCacheService = documentCacheService;
_mediaCacheService = mediaCacheService;
_contentTypeCommonRepository = contentTypeCommonRepository;
}
[Obsolete("Use the non-obsolete constructor instead. Scheduled for removal in V18.")]
public DataTypeCacheRefresher(
AppCaches appCaches,
IJsonSerializer serializer,
IIdKeyMap idKeyMap,
IEventAggregator eventAggregator,
ICacheRefresherNotificationFactory factory,
IPublishedModelFactory publishedModelFactory,
IPublishedContentTypeFactory publishedContentTypeFactory,
IPublishedContentTypeCache publishedContentTypeCache,
IDocumentCacheService documentCacheService,
IMediaCacheService mediaCacheService)
: this(
appCaches,
serializer,
idKeyMap,
eventAggregator,
factory,
publishedModelFactory,
publishedContentTypeFactory,
publishedContentTypeCache,
documentCacheService,
mediaCacheService,
StaticServiceProvider.Instance.GetRequiredService<IContentTypeCommonRepository>())
{
}
#region Json
@@ -84,6 +116,9 @@ public sealed class DataTypeCacheRefresher : PayloadCacheRefresherBase<DataTypeC
ClearAllIsolatedCacheByEntityType<IMember>();
ClearAllIsolatedCacheByEntityType<IMemberType>();
// Also clear the 5 minute runtime cache held in ContentTypeCommonRepository.
_contentTypeCommonRepository.ClearCache();
Attempt<IAppPolicyCache?> dataTypeCache = AppCaches.IsolatedCaches.Get<IDataType>();
foreach (JsonPayload payload in payloads)
+5 -2
View File
@@ -29,7 +29,7 @@ public class TypeFinder : ITypeFinder
"DataAnnotationsExtensions,", "DataAnnotationsExtensions.", "Dynamic,", "Examine,", "Examine.",
"HtmlAgilityPack,", "HtmlAgilityPack.", "HtmlDiff,", "ICSharpCode.", "Iesi.Collections,", // used by NHibernate
"JetBrains.Annotations,", "LightInject.", // DI
"LightInject,", "Lucene.", "Markdown,", "Microsoft.", "MiniProfiler,", "Moq,", "MySql.", "NHibernate,",
"LightInject,", "Lucene.", "Markdig,", "Markdown,", "Microsoft.", "MiniProfiler,", "Moq,", "MySql.", "NHibernate,",
"NHibernate.", "Newtonsoft.", "NPoco,", "NuGet.", "RouteDebugger,", "Semver.", "Serilog.", "Serilog,",
"ServiceStack.", "SqlCE4Umbraco,", "Superpower,", // used by Serilog
"System.", "TidyNet,", "TidyNet.", "WebDriver,", "itextsharp,", "mscorlib,", "NUnit,", "NUnit.", "NUnit3.",
@@ -115,7 +115,10 @@ public class TypeFinder : ITypeFinder
{
IEnumerable<Assembly> assemblyList = assemblies ?? AssembliesToScan;
return GetClassesWithBaseType(assignTypeFrom, assemblyList, onlyConcreteClasses,
return GetClassesWithBaseType(
assignTypeFrom,
assemblyList,
onlyConcreteClasses,
// the additional filter will ensure that any found types also have the attribute applied.
t => t.GetCustomAttributes(attributeType, false).Any());
+90 -156
View File
@@ -1,9 +1,6 @@
using System.Reflection;
using System.Runtime.Serialization;
using Microsoft.Extensions.Logging;
using Umbraco.Cms.Core.Cache;
using Umbraco.Cms.Core.Collections;
using Umbraco.Cms.Core.Logging;
using Umbraco.Extensions;
namespace Umbraco.Cms.Core.Composing;
@@ -20,13 +17,21 @@ namespace Umbraco.Cms.Core.Composing;
/// </remarks>
public sealed class TypeLoader
{
private readonly Lock _locko = new();
private readonly Lock _typesLock = new();
private readonly ILogger<TypeLoader> _logger;
private readonly Dictionary<CompositeTypeTypeKey, TypeList> _types = new();
private IEnumerable<Assembly>? _assemblies;
private bool IsDebugEnabled => _logger.IsEnabled(Microsoft.Extensions.Logging.LogLevel.Debug);
/// <summary>
/// Initializes a new instance of the <see cref="TypeLoader"/> class.
/// </summary>
/// <param name="typeFinder">The type finder used to discover types.</param>
/// <param name="logger">The logger instance.</param>
/// <param name="assembliesToScan">Optional set of assemblies to scan.</param>
public TypeLoader(
ITypeFinder typeFinder,
ILogger<TypeLoader> logger,
@@ -105,10 +110,7 @@ public sealed class TypeLoader
/// <remarks>Caching is disabled when using specific assemblies.</remarks>
public IEnumerable<Type> GetTypes<T>(bool cache = true, IEnumerable<Assembly>? specificAssemblies = null)
{
if (_logger == null)
{
throw new InvalidOperationException("Cannot get types from a test/blank type loader.");
}
EnsureInitialized();
// do not cache anything from specific assemblies
cache &= specificAssemblies == null;
@@ -116,14 +118,11 @@ public sealed class TypeLoader
// if not IDiscoverable, directly get types
if (!typeof(IDiscoverable).IsAssignableFrom(typeof(T)))
{
// warn
if (_logger.IsEnabled(Microsoft.Extensions.Logging.LogLevel.Debug))
{
_logger.LogDebug(
"Running a full, " + (cache ? string.Empty : "non-") +
"cached, scan for non-discoverable type {TypeName} (slow).",
LogDebugIf(
true,
"Running a full, {CacheStatus}cached, scan for non-discoverable type {TypeName} (slow).",
CacheStatus(cache),
typeof(T).FullName);
}
return GetTypesInternal(
typeof(T),
@@ -134,23 +133,12 @@ public sealed class TypeLoader
}
// get IDiscoverable and always cache
IEnumerable<Type> discovered = GetTypesInternal(
typeof(IDiscoverable),
null,
() => TypeFinder.FindClassesOfType<IDiscoverable>(AssembliesToScan),
"scanning assemblies",
true);
IEnumerable<Type> discovered = GetDiscoverableTypes();
// warn
if (!cache)
{
if (_logger.IsEnabled(Microsoft.Extensions.Logging.LogLevel.Debug))
{
_logger.LogDebug(
"Running a non-cached, filter for discoverable type {TypeName} (slowish).",
typeof(T).FullName);
}
}
LogDebugIf(
!cache,
"Running a non-cached, filter for discoverable type {TypeName} (slowish).",
typeof(T).FullName);
// filter the cached discovered types (and maybe cache the result)
return GetTypesInternal(
@@ -175,10 +163,7 @@ public sealed class TypeLoader
IEnumerable<Assembly>? specificAssemblies = null)
where TAttribute : Attribute
{
if (_logger == null)
{
throw new InvalidOperationException("Cannot get types from a test/blank type loader.");
}
EnsureInitialized();
// do not cache anything from specific assemblies
cache &= specificAssemblies == null;
@@ -186,14 +171,12 @@ public sealed class TypeLoader
// if not IDiscoverable, directly get types
if (!typeof(IDiscoverable).IsAssignableFrom(typeof(T)))
{
if (_logger.IsEnabled(Microsoft.Extensions.Logging.LogLevel.Debug))
{
_logger.LogDebug(
"Running a full, " + (cache ? string.Empty : "non-") +
"cached, scan for non-discoverable type {TypeName} / attribute {AttributeName} (slow).",
LogDebugIf(
true,
"Running a full, {CacheStatus}cached, scan for non-discoverable type {TypeName} / attribute {AttributeName} (slow).",
CacheStatus(cache),
typeof(T).FullName,
typeof(TAttribute).FullName);
}
return GetTypesInternal(
typeof(T),
@@ -204,24 +187,13 @@ public sealed class TypeLoader
}
// get IDiscoverable and always cache
IEnumerable<Type> discovered = GetTypesInternal(
typeof(IDiscoverable),
null,
() => TypeFinder.FindClassesOfType<IDiscoverable>(AssembliesToScan),
"scanning assemblies",
true);
IEnumerable<Type> discovered = GetDiscoverableTypes();
// warn
if (!cache)
{
if (_logger.IsEnabled(Microsoft.Extensions.Logging.LogLevel.Debug))
{
_logger.LogDebug(
"Running a non-cached, filter for discoverable type {TypeName} / attribute {AttributeName} (slowish).",
typeof(T).FullName,
typeof(TAttribute).FullName);
}
}
LogDebugIf(
!cache,
"Running a non-cached, filter for discoverable type {TypeName} / attribute {AttributeName} (slowish).",
typeof(T).FullName,
typeof(TAttribute).FullName);
// filter the cached discovered types (and maybe cache the result)
return GetTypesInternal(
@@ -247,23 +219,15 @@ public sealed class TypeLoader
IEnumerable<Assembly>? specificAssemblies = null)
where TAttribute : Attribute
{
if (_logger == null)
{
throw new InvalidOperationException("Cannot get types from a test/blank type loader.");
}
EnsureInitialized();
// do not cache anything from specific assemblies
cache &= specificAssemblies == null;
if (!cache)
{
if (_logger.IsEnabled(Microsoft.Extensions.Logging.LogLevel.Debug))
{
_logger.LogDebug(
"Running a full, non-cached, scan for types / attribute {AttributeName} (slow).",
typeof(TAttribute).FullName);
}
}
LogDebugIf(
!cache,
"Running a full, non-cached, scan for types / attribute {AttributeName} (slow).",
typeof(TAttribute).FullName);
return GetTypesInternal(
typeof(object),
@@ -280,6 +244,38 @@ public sealed class TypeLoader
return s;
}
private void EnsureInitialized()
{
if (_logger == null)
{
throw new InvalidOperationException("Cannot get types from a test/blank type loader.");
}
}
private IEnumerable<Type> GetDiscoverableTypes() =>
GetTypesInternal(
typeof(IDiscoverable),
null,
() => TypeFinder.FindClassesOfType<IDiscoverable>(AssembliesToScan),
"scanning assemblies",
true);
/// <summary>
/// Logs a debug message if the specified condition is true and debug logging is enabled.
/// </summary>
/// <param name="condition">The condition that must be true to log.</param>
/// <param name="message">The log message template.</param>
/// <param name="args">The message arguments.</param>
private void LogDebugIf(bool condition, string message, params object?[] args)
{
if (condition && IsDebugEnabled)
{
_logger.LogDebug(message, args);
}
}
private string CacheStatus(bool cache) => cache ? string.Empty : "non-";
private IEnumerable<Type> GetTypesInternal(
Type baseType,
Type? attributeType,
@@ -291,7 +287,7 @@ public sealed class TypeLoader
// lock at a time, and we don't have non-upgradeable readers, and quite probably the type
// loader is mostly not going to be used in any kind of massively multi-threaded scenario - so,
// a plain lock is enough
lock (_locko)
lock (_typesLock)
{
return GetTypesInternalLocked(baseType, attributeType, finder, action, cache);
}
@@ -305,34 +301,21 @@ public sealed class TypeLoader
bool cache)
{
// check if the TypeList already exists, if so return it, if not we'll create it
Type tobject = typeof(object); // CompositeTypeTypeKey does not support null values
var listKey = new CompositeTypeTypeKey(baseType ?? tobject, attributeType ?? tobject);
TypeList? typeList = null;
Type objectType = typeof(object); // CompositeTypeTypeKey does not support null values
var listKey = new CompositeTypeTypeKey(baseType ?? objectType, attributeType ?? objectType);
if (cache)
// need to put some logging here to try to figure out why this is happening: http://issues.umbraco.org/issue/U4-3505
if (cache && _types.TryGetValue(listKey, out TypeList? cachedList))
{
_types.TryGetValue(listKey, out typeList); // else null
LogDebugIf(true, "Getting {TypeName}: found a cached type list.", GetName(baseType, attributeType));
return cachedList.Types;
}
// if caching and found, return
if (typeList != null)
{
// need to put some logging here to try to figure out why this is happening: http://issues.umbraco.org/issue/U4-3505
if (_logger.IsEnabled(Microsoft.Extensions.Logging.LogLevel.Debug))
{
_logger.LogDebug("Getting {TypeName}: found a cached type list.", GetName(baseType, attributeType));
}
return typeList.Types;
}
// else proceed,
typeList = new TypeList(baseType, attributeType);
// else proceed
var typeList = new TypeList(baseType, attributeType);
// either we had to scan, or we could not get the types from the cache file - scan now
if (_logger.IsEnabled(Microsoft.Extensions.Logging.LogLevel.Debug))
{
_logger.LogDebug("Getting {TypeName}: " + action + ".", GetName(baseType, attributeType));
}
LogDebugIf(true, "Getting {TypeName}: " + action + ".", GetName(baseType, attributeType));
foreach (Type t in finder())
{
@@ -343,17 +326,11 @@ public sealed class TypeLoader
if (cache)
{
var added = _types.TryAdd(listKey, typeList);
if (_logger.IsEnabled(Microsoft.Extensions.Logging.LogLevel.Debug))
{
_logger.LogDebug("Got {TypeName}, caching ({CacheType}).", GetName(baseType, attributeType), added.ToString().ToLowerInvariant());
}
LogDebugIf(true, "Got {TypeName}, caching ({CacheType}).", GetName(baseType, attributeType), added.ToString().ToLowerInvariant());
}
else
{
if (_logger.IsEnabled(Microsoft.Extensions.Logging.LogLevel.Debug))
{
_logger.LogDebug("Got {TypeName}.", GetName(baseType, attributeType));
}
LogDebugIf(true, "Got {TypeName}.", GetName(baseType, attributeType));
}
return typeList.Types;
@@ -371,14 +348,25 @@ public sealed class TypeLoader
{
private readonly HashSet<Type> _types = new();
/// <summary>
/// Initializes a new instance of the <see cref="TypeList"/> class.
/// </summary>
/// <param name="baseType">The base type to filter by.</param>
/// <param name="attributeType">The attribute type to filter by.</param>
public TypeList(Type? baseType, Type? attributeType)
{
BaseType = baseType;
AttributeType = attributeType;
}
/// <summary>
/// Gets the base type used for filtering.
/// </summary>
public Type? BaseType { get; }
/// <summary>
/// Gets the attribute type used for filtering.
/// </summary>
public Type? AttributeType { get; }
/// <summary>
@@ -402,59 +390,5 @@ public sealed class TypeLoader
}
}
/// <summary>
/// Represents the error that occurs when a type was not found in the cache type list with the specified
/// TypeResolutionKind.
/// </summary>
/// <seealso cref="System.Exception" />
[Serializable]
internal sealed class CachedTypeNotFoundInFileException : Exception
{
/// <summary>
/// Initializes a new instance of the <see cref="CachedTypeNotFoundInFileException" /> class.
/// </summary>
public CachedTypeNotFoundInFileException()
{
}
/// <summary>
/// Initializes a new instance of the <see cref="CachedTypeNotFoundInFileException" /> class.
/// </summary>
/// <param name="message">The message that describes the error.</param>
public CachedTypeNotFoundInFileException(string message)
: base(message)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="CachedTypeNotFoundInFileException" /> class.
/// </summary>
/// <param name="message">The error message that explains the reason for the exception.</param>
/// <param name="innerException">
/// The exception that is the cause of the current exception, or a null reference (
/// <see langword="Nothing" /> in Visual Basic) if no inner exception is specified.
/// </param>
public CachedTypeNotFoundInFileException(string message, Exception innerException)
: base(message, innerException)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="CachedTypeNotFoundInFileException" /> class.
/// </summary>
/// <param name="info">
/// The <see cref="T:System.Runtime.Serialization.SerializationInfo" /> that holds the serialized object
/// data about the exception being thrown.
/// </param>
/// <param name="context">
/// The <see cref="T:System.Runtime.Serialization.StreamingContext" /> that contains contextual
/// information about the source or destination.
/// </param>
protected CachedTypeNotFoundInFileException(SerializationInfo info, StreamingContext context)
: base(info, context)
{
}
}
#endregion
}
@@ -10,9 +10,12 @@ namespace Umbraco.Cms.Core.Configuration.Models;
/// </summary>
public class ContentImagingSettings
{
/// <summary>
/// The default set of accepted image file extensions.
/// </summary>
internal const string StaticImageFileTypes = "jpeg,jpg,gif,bmp,png,tiff,tif,webp";
private static readonly ISet<ImagingAutoFillUploadField> DefaultImagingAutoFillUploadField = new HashSet<ImagingAutoFillUploadField>
private static readonly ISet<ImagingAutoFillUploadField> _defaultImagingAutoFillUploadField = new HashSet<ImagingAutoFillUploadField>
{
new()
{
@@ -31,8 +34,7 @@ public class ContentImagingSettings
public ISet<string> ImageFileTypes { get; set; } = new HashSet<string>(StaticImageFileTypes.Split(Constants.CharArrays.Comma));
/// <summary>
/// Gets or sets a value for the imaging autofill following media file upload fields.
/// Gets or sets the collection of media property mappings that are automatically populated with image metadata after a media file is uploaded.
/// </summary>
/// <value>
public ISet<ImagingAutoFillUploadField> AutoFillImageProperties { get; set; } = DefaultImagingAutoFillUploadField;
public ISet<ImagingAutoFillUploadField> AutoFillImageProperties { get; set; } = _defaultImagingAutoFillUploadField;
}
@@ -49,6 +49,7 @@ public class AuthorizationException : Exception
/// The <see cref="T:System.Runtime.Serialization.StreamingContext" /> that contains contextual
/// information about the source or destination.
/// </param>
[Obsolete("Constructors taking a signature of SerializationInfo info, StreamingContext context are deprecated and not used within Umbraco. Scheduled for removal in Umbraco 19.", DiagnosticId = "SYSLIB0051")]
protected AuthorizationException(SerializationInfo info, StreamingContext context)
: base(info, context)
{
@@ -23,7 +23,7 @@ public class BootFailedException : Exception
}
/// <summary>
/// Initializes a new instance of the <see cref="Exception" /> class with a specified error message.
/// Initializes a new instance of the <see cref="BootFailedException" /> class with a specified error message.
/// </summary>
/// <param name="message">The message that describes the error.</param>
public BootFailedException(string message)
@@ -32,7 +32,7 @@ public class BootFailedException : Exception
}
/// <summary>
/// Initializes a new instance of the <see cref="Exception" /> class with a specified error message
/// Initializes a new instance of the <see cref="BootFailedException" /> class with a specified error message
/// and a reference to the inner exception which is the cause of this exception.
/// </summary>
/// <param name="message">The message that describes the error.</param>
@@ -53,6 +53,7 @@ public class BootFailedException : Exception
/// The <see cref="T:System.Runtime.Serialization.StreamingContext" /> that contains contextual
/// information about the source or destination.
/// </param>
[Obsolete("Constructors taking a signature of SerializationInfo info, StreamingContext context are deprecated and not used within Umbraco. Scheduled for removal in Umbraco 19.", DiagnosticId = "SYSLIB0051")]
protected BootFailedException(SerializationInfo info, StreamingContext context)
: base(info, context)
{
@@ -61,9 +62,8 @@ public class BootFailedException : Exception
/// <summary>
/// Rethrows a captured <see cref="BootFailedException" />.
/// </summary>
/// <param name="bootFailedException">The boot failed exception.</param>
/// <exception cref="BootFailedException">
/// </exception>
/// <param name="bootFailedException">The boot failed exception to rethrow, or null to throw with a default message.</param>
/// <exception cref="BootFailedException">Always thrown with the original exception details or a default message.</exception>
/// <remarks>
/// The exception can be null, in which case a default message is used.
/// </remarks>
@@ -40,6 +40,7 @@ public class ConfigurationException : Exception
/// The <see cref="T:System.Runtime.Serialization.StreamingContext" /> that contains contextual
/// information about the source or destination.
/// </param>
[Obsolete("Constructors taking a signature of SerializationInfo info, StreamingContext context are deprecated and not used within Umbraco. Scheduled for removal in Umbraco 19.", DiagnosticId = "SYSLIB0051")]
protected ConfigurationException(SerializationInfo info, StreamingContext context)
: base(info, context)
{
@@ -50,6 +50,7 @@ public class PanicException : Exception
/// The <see cref="T:System.Runtime.Serialization.StreamingContext" /> that contains contextual
/// information about the source or destination.
/// </param>
[Obsolete("Constructors taking a signature of SerializationInfo info, StreamingContext context are deprecated and not used within Umbraco. Scheduled for removal in Umbraco 19.", DiagnosticId = "SYSLIB0051")]
protected PanicException(SerializationInfo info, StreamingContext context)
: base(info, context)
{
@@ -5,6 +5,7 @@ namespace Umbraco.Cms.Core.Exceptions;
/// <summary>
/// An exception that is thrown if an unattended installation occurs.
/// </summary>
/// <seealso cref="System.Exception" />
[Serializable]
public class UnattendedInstallException : Exception
{
@@ -46,6 +47,7 @@ public class UnattendedInstallException : Exception
/// The <see cref="T:System.Runtime.Serialization.StreamingContext" /> that contains contextual
/// information about the source or destination.
/// </param>
[Obsolete("Constructors taking a signature of SerializationInfo info, StreamingContext context are deprecated and not used within Umbraco. Scheduled for removal in Umbraco 19.", DiagnosticId = "SYSLIB0051")]
protected UnattendedInstallException(SerializationInfo info, StreamingContext context)
: base(info, context)
{
@@ -15,6 +15,10 @@ using Umbraco.Cms.Core.Services.Navigation;
namespace Umbraco.Extensions;
/// <summary>
/// Provides extension methods for <see cref="IPublishedContent"/> to navigate content trees,
/// access properties, and retrieve related content such as ancestors, descendants, siblings, and children.
/// </summary>
public static class PublishedContentExtensions
{
#region Name
@@ -267,9 +271,26 @@ public static class PublishedContentExtensions
return template?.Alias ?? string.Empty;
}
/// <summary>
/// Determines whether a specific template is allowed for the content item.
/// </summary>
/// <param name="content">The content item.</param>
/// <param name="contentTypeService">The content type service.</param>
/// <param name="webRoutingSettings">The web routing settings.</param>
/// <param name="templateId">The template identifier.</param>
/// <returns><c>true</c> if the template is allowed; otherwise, <c>false</c>.</returns>
public static bool IsAllowedTemplate(this IPublishedContent content, IContentTypeService contentTypeService, WebRoutingSettings webRoutingSettings, int templateId) =>
content.IsAllowedTemplate(contentTypeService, webRoutingSettings.DisableAlternativeTemplates, webRoutingSettings.ValidateAlternativeTemplates, templateId);
/// <summary>
/// Determines whether a specific template is allowed for the content item.
/// </summary>
/// <param name="content">The content item.</param>
/// <param name="contentTypeService">The content type service.</param>
/// <param name="disableAlternativeTemplates">Whether alternative templates are disabled.</param>
/// <param name="validateAlternativeTemplates">Whether to validate alternative templates against allowed templates.</param>
/// <param name="templateId">The template identifier.</param>
/// <returns><c>true</c> if the template is allowed; otherwise, <c>false</c>.</returns>
public static bool IsAllowedTemplate(this IPublishedContent content, IContentTypeService contentTypeService, bool disableAlternativeTemplates, bool validateAlternativeTemplates, int templateId)
{
if (disableAlternativeTemplates)
@@ -292,6 +313,16 @@ public static class PublishedContentExtensions
return publishedContentContentType.IsAllowedTemplate(templateId);
}
/// <summary>
/// Determines whether a specific template is allowed for the content item by template alias.
/// </summary>
/// <param name="content">The content item.</param>
/// <param name="fileService">The file service.</param>
/// <param name="contentTypeService">The content type service.</param>
/// <param name="disableAlternativeTemplates">Whether alternative templates are disabled.</param>
/// <param name="validateAlternativeTemplates">Whether to validate alternative templates against allowed templates.</param>
/// <param name="templateAlias">The template alias.</param>
/// <returns><c>true</c> if the template is allowed; otherwise, <c>false</c>.</returns>
public static bool IsAllowedTemplate(this IPublishedContent content, IFileService fileService, IContentTypeService contentTypeService, bool disableAlternativeTemplates, bool validateAlternativeTemplates, string templateAlias)
{
ITemplate? template = fileService.GetTemplate(templateAlias);
@@ -443,8 +474,20 @@ public static class PublishedContentExtensions
#region IsSomething: equality
/// <summary>
/// Determines whether this content item is equal to another content item by comparing their IDs.
/// </summary>
/// <param name="content">The content item.</param>
/// <param name="other">The other content item to compare.</param>
/// <returns><c>true</c> if both content items have the same ID; otherwise, <c>false</c>.</returns>
public static bool IsEqual(this IPublishedContent content, IPublishedContent other) => content.Id == other.Id;
/// <summary>
/// Determines whether this content item is not equal to another content item by comparing their IDs.
/// </summary>
/// <param name="content">The content item.</param>
/// <param name="other">The other content item to compare.</param>
/// <returns><c>true</c> if the content items have different IDs; otherwise, <c>false</c>.</returns>
public static bool IsNotEqual(this IPublishedContent content, IPublishedContent other) =>
content.IsEqual(other) == false;
@@ -452,15 +495,39 @@ public static class PublishedContentExtensions
#region IsSomething: ancestors and descendants
/// <summary>
/// Determines whether this content item is a descendant of another content item.
/// </summary>
/// <param name="content">The content item.</param>
/// <param name="other">The potential ancestor content item.</param>
/// <returns><c>true</c> if this content is a descendant of the other; otherwise, <c>false</c>.</returns>
public static bool IsDescendant(this IPublishedContent content, IPublishedContent other) =>
other.Level < content.Level && content.Path.InvariantStartsWith(other.Path.EnsureEndsWith(','));
/// <summary>
/// Determines whether this content item is a descendant of or the same as another content item.
/// </summary>
/// <param name="content">The content item.</param>
/// <param name="other">The potential ancestor or same content item.</param>
/// <returns><c>true</c> if this content is a descendant of or equal to the other; otherwise, <c>false</c>.</returns>
public static bool IsDescendantOrSelf(this IPublishedContent content, IPublishedContent other) =>
content.Path.InvariantEquals(other.Path) || content.IsDescendant(other);
/// <summary>
/// Determines whether this content item is an ancestor of another content item.
/// </summary>
/// <param name="content">The content item.</param>
/// <param name="other">The potential descendant content item.</param>
/// <returns><c>true</c> if this content is an ancestor of the other; otherwise, <c>false</c>.</returns>
public static bool IsAncestor(this IPublishedContent content, IPublishedContent other) =>
content.Level < other.Level && other.Path.InvariantStartsWith(content.Path.EnsureEndsWith(','));
/// <summary>
/// Determines whether this content item is an ancestor of or the same as another content item.
/// </summary>
/// <param name="content">The content item.</param>
/// <param name="other">The potential descendant or same content item.</param>
/// <returns><c>true</c> if this content is an ancestor of or equal to the other; otherwise, <c>false</c>.</returns>
public static bool IsAncestorOrSelf(this IPublishedContent content, IPublishedContent other) =>
other.Path.InvariantEquals(content.Path) || content.IsAncestor(other);
@@ -571,7 +638,8 @@ public static class PublishedContentExtensions
/// </summary>
/// <typeparam name="T">The content type.</typeparam>
/// <param name="content">The content.</param>
/// <param name="publishedStatusFilteringService"></param>
/// <param name="navigationQueryService">The query service for the in-memory navigation structure.</param>
/// <param name="publishedStatusFilteringService">The service for filtering published content by status.</param>
/// <param name="maxLevel">The level.</param>
/// <returns>
/// The ancestors of the content, at a level lesser or equal to the specified level, and of the specified
@@ -871,6 +939,15 @@ public static class PublishedContentExtensions
where T : class, IPublishedContent
=> content.AncestorsOrSelf<T>(navigationQueryService, publishedStatusFilteringService, maxLevel).FirstOrDefault();
/// <summary>
/// Gets the ancestors or self of the content item, optionally filtered by a predicate.
/// </summary>
/// <param name="content">The content item.</param>
/// <param name="navigationQueryService">The navigation query service.</param>
/// <param name="publishedStatusFilteringService">The published status filtering service.</param>
/// <param name="orSelf">Whether to include the content item itself.</param>
/// <param name="func">An optional predicate to filter the ancestors.</param>
/// <returns>An enumerable of ancestors or self matching the criteria.</returns>
public static IEnumerable<IPublishedContent> AncestorsOrSelf(
this IPublishedContent content,
INavigationQueryService navigationQueryService,
@@ -984,6 +1061,15 @@ public static class PublishedContentExtensions
// - every node occurs before all of its children and descendants.
// - the relative order of siblings is the order in which they occur in the children property of their parent node.
// - children and descendants occur before following siblings.
/// <summary>
/// Gets all descendants of the content item.
/// </summary>
/// <param name="content">The content item.</param>
/// <param name="navigationQueryService">The navigation query service.</param>
/// <param name="publishedStatusFilteringService">The published status filtering service.</param>
/// <param name="culture">The culture for variant content.</param>
/// <returns>An enumerable of all descendant content items.</returns>
public static IEnumerable<IPublishedContent> Descendants(
this IPublishedContent content,
INavigationQueryService navigationQueryService,
@@ -991,6 +1077,15 @@ public static class PublishedContentExtensions
string? culture = null)
=> content.DescendantsOrSelf(navigationQueryService, publishedStatusFilteringService, false, null, culture);
/// <summary>
/// Gets all descendants of the content item at or above a specified level.
/// </summary>
/// <param name="content">The content item.</param>
/// <param name="navigationQueryService">The navigation query service.</param>
/// <param name="publishedStatusFilteringService">The published status filtering service.</param>
/// <param name="level">The minimum level of descendants to return.</param>
/// <param name="culture">The culture for variant content.</param>
/// <returns>An enumerable of descendant content items at or above the specified level.</returns>
public static IEnumerable<IPublishedContent> Descendants(
this IPublishedContent content,
INavigationQueryService navigationQueryService,
@@ -1004,6 +1099,15 @@ public static class PublishedContentExtensions
p => p.Level >= level,
culture);
/// <summary>
/// Gets all descendants of the content item of a specific content type.
/// </summary>
/// <param name="content">The content item.</param>
/// <param name="navigationQueryService">The navigation query service.</param>
/// <param name="publishedStatusFilteringService">The published status filtering service.</param>
/// <param name="contentTypeAlias">The content type alias to filter by.</param>
/// <param name="culture">The culture for variant content.</param>
/// <returns>An enumerable of descendant content items of the specified type.</returns>
public static IEnumerable<IPublishedContent> DescendantsOfType(
this IPublishedContent content,
INavigationQueryService navigationQueryService,
@@ -1017,6 +1121,15 @@ public static class PublishedContentExtensions
false,
contentTypeAlias);
/// <summary>
/// Gets all descendants of the content item of a specific type.
/// </summary>
/// <typeparam name="T">The type of content to return.</typeparam>
/// <param name="content">The content item.</param>
/// <param name="navigationQueryService">The navigation query service.</param>
/// <param name="publishedStatusFilteringService">The published status filtering service.</param>
/// <param name="culture">The culture for variant content.</param>
/// <returns>An enumerable of descendant content items of the specified type.</returns>
public static IEnumerable<T> Descendants<T>(
this IPublishedContent content,
INavigationQueryService navigationQueryService,
@@ -1025,6 +1138,16 @@ public static class PublishedContentExtensions
where T : class, IPublishedContent
=> content.Descendants(navigationQueryService, publishedStatusFilteringService, culture).OfType<T>();
/// <summary>
/// Gets all descendants of the content item of a specific type at or above a specified level.
/// </summary>
/// <typeparam name="T">The type of content to return.</typeparam>
/// <param name="content">The content item.</param>
/// <param name="navigationQueryService">The navigation query service.</param>
/// <param name="publishedStatusFilteringService">The published status filtering service.</param>
/// <param name="level">The minimum level of descendants to return.</param>
/// <param name="culture">The culture for variant content.</param>
/// <returns>An enumerable of descendant content items of the specified type at or above the level.</returns>
public static IEnumerable<T> Descendants<T>(
this IPublishedContent content,
INavigationQueryService navigationQueryService,
@@ -1034,6 +1157,14 @@ public static class PublishedContentExtensions
where T : class, IPublishedContent
=> content.Descendants(navigationQueryService, publishedStatusFilteringService, level, culture).OfType<T>();
/// <summary>
/// Gets all descendants of the content item including itself.
/// </summary>
/// <param name="content">The content item.</param>
/// <param name="navigationQueryService">The navigation query service.</param>
/// <param name="publishedStatusFilteringService">The published status filtering service.</param>
/// <param name="culture">The culture for variant content.</param>
/// <returns>An enumerable of the content item and all its descendants.</returns>
public static IEnumerable<IPublishedContent> DescendantsOrSelf(
this IPublishedContent content,
INavigationQueryService navigationQueryService,
@@ -1041,6 +1172,15 @@ public static class PublishedContentExtensions
string? culture = null)
=> content.DescendantsOrSelf(navigationQueryService, publishedStatusFilteringService, true, null, culture);
/// <summary>
/// Gets all descendants of the content item including itself at or above a specified level.
/// </summary>
/// <param name="content">The content item.</param>
/// <param name="navigationQueryService">The navigation query service.</param>
/// <param name="publishedStatusFilteringService">The published status filtering service.</param>
/// <param name="level">The minimum level to return.</param>
/// <param name="culture">The culture for variant content.</param>
/// <returns>An enumerable of the content item and all its descendants at or above the level.</returns>
public static IEnumerable<IPublishedContent> DescendantsOrSelf(
this IPublishedContent content,
INavigationQueryService navigationQueryService,
@@ -1049,6 +1189,15 @@ public static class PublishedContentExtensions
string? culture = null)
=> content.DescendantsOrSelf(navigationQueryService, publishedStatusFilteringService, true, p => p.Level >= level, culture);
/// <summary>
/// Gets all descendants of the content item including itself of a specific content type.
/// </summary>
/// <param name="content">The content item.</param>
/// <param name="navigationQueryService">The navigation query service.</param>
/// <param name="publishedStatusFilteringService">The published status filtering service.</param>
/// <param name="contentTypeAlias">The content type alias to filter by.</param>
/// <param name="culture">The culture for variant content.</param>
/// <returns>An enumerable of the content item and its descendants of the specified type.</returns>
public static IEnumerable<IPublishedContent> DescendantsOrSelfOfType(
this IPublishedContent content,
INavigationQueryService navigationQueryService,
@@ -1062,6 +1211,15 @@ public static class PublishedContentExtensions
true,
contentTypeAlias);
/// <summary>
/// Gets all descendants of the content item including itself of a specific type.
/// </summary>
/// <typeparam name="T">The type of content to return.</typeparam>
/// <param name="content">The content item.</param>
/// <param name="navigationQueryService">The navigation query service.</param>
/// <param name="publishedStatusFilteringService">The published status filtering service.</param>
/// <param name="culture">The culture for variant content.</param>
/// <returns>An enumerable of the content item and its descendants of the specified type.</returns>
public static IEnumerable<T> DescendantsOrSelf<T>(
this IPublishedContent content,
INavigationQueryService navigationQueryService,
@@ -1070,6 +1228,16 @@ public static class PublishedContentExtensions
where T : class, IPublishedContent
=> content.DescendantsOrSelf(navigationQueryService, publishedStatusFilteringService, culture).OfType<T>();
/// <summary>
/// Gets all descendants of the content item including itself of a specific type at or above a specified level.
/// </summary>
/// <typeparam name="T">The type of content to return.</typeparam>
/// <param name="content">The content item.</param>
/// <param name="navigationQueryService">The navigation query service.</param>
/// <param name="publishedStatusFilteringService">The published status filtering service.</param>
/// <param name="level">The minimum level to return.</param>
/// <param name="culture">The culture for variant content.</param>
/// <returns>An enumerable of the content item and its descendants of the specified type at or above the level.</returns>
public static IEnumerable<T> DescendantsOrSelf<T>(
this IPublishedContent content,
INavigationQueryService navigationQueryService,
@@ -1079,6 +1247,14 @@ public static class PublishedContentExtensions
where T : class, IPublishedContent
=> content.DescendantsOrSelf(navigationQueryService, publishedStatusFilteringService, level, culture).OfType<T>();
/// <summary>
/// Gets the first descendant of the content item.
/// </summary>
/// <param name="content">The content item.</param>
/// <param name="navigationQueryService">The navigation query service.</param>
/// <param name="publishedStatusFilteringService">The published status filtering service.</param>
/// <param name="culture">The culture for variant content.</param>
/// <returns>The first descendant, or null if none exists.</returns>
public static IPublishedContent? Descendant(
this IPublishedContent content,
INavigationQueryService navigationQueryService,
@@ -1086,6 +1262,15 @@ public static class PublishedContentExtensions
string? culture = null)
=> content.Children(navigationQueryService, publishedStatusFilteringService, culture)?.FirstOrDefault();
/// <summary>
/// Gets the first descendant of the content item at a specified level.
/// </summary>
/// <param name="content">The content item.</param>
/// <param name="navigationQueryService">The navigation query service.</param>
/// <param name="publishedStatusFilteringService">The published status filtering service.</param>
/// <param name="level">The level to find a descendant at.</param>
/// <param name="culture">The culture for variant content.</param>
/// <returns>The first descendant at the specified level, or null if none exists.</returns>
public static IPublishedContent? Descendant(
this IPublishedContent content,
INavigationQueryService navigationQueryService,
@@ -1096,6 +1281,15 @@ public static class PublishedContentExtensions
.EnumerateDescendants(navigationQueryService, publishedStatusFilteringService, false, culture)
.FirstOrDefault(x => x.Level == level);
/// <summary>
/// Gets the first descendant of the content item of a specific content type.
/// </summary>
/// <param name="content">The content item.</param>
/// <param name="navigationQueryService">The navigation query service.</param>
/// <param name="publishedStatusFilteringService">The published status filtering service.</param>
/// <param name="contentTypeAlias">The content type alias to filter by.</param>
/// <param name="culture">The culture for variant content.</param>
/// <returns>The first descendant of the specified type, or null if none exists.</returns>
public static IPublishedContent? DescendantOfType(
this IPublishedContent content,
INavigationQueryService navigationQueryService,
@@ -1111,6 +1305,15 @@ public static class PublishedContentExtensions
contentTypeAlias)
.FirstOrDefault();
/// <summary>
/// Gets the first descendant of the content item of a specific type.
/// </summary>
/// <typeparam name="T">The type of content to return.</typeparam>
/// <param name="content">The content item.</param>
/// <param name="navigationQueryService">The navigation query service.</param>
/// <param name="publishedStatusFilteringService">The published status filtering service.</param>
/// <param name="culture">The culture for variant content.</param>
/// <returns>The first descendant of the specified type, or null if none exists.</returns>
public static T? Descendant<T>(
this IPublishedContent content,
INavigationQueryService navigationQueryService,
@@ -1121,6 +1324,16 @@ public static class PublishedContentExtensions
.EnumerateDescendants(navigationQueryService, publishedStatusFilteringService, false, culture)
.FirstOrDefault(x => x is T) as T;
/// <summary>
/// Gets the first descendant of the content item of a specific type at a specified level.
/// </summary>
/// <typeparam name="T">The type of content to return.</typeparam>
/// <param name="content">The content item.</param>
/// <param name="navigationQueryService">The navigation query service.</param>
/// <param name="publishedStatusFilteringService">The published status filtering service.</param>
/// <param name="level">The level to find a descendant at.</param>
/// <param name="culture">The culture for variant content.</param>
/// <returns>The first descendant of the specified type at the level, or null if none exists.</returns>
public static T? Descendant<T>(
this IPublishedContent content,
INavigationQueryService navigationQueryService,
@@ -1130,6 +1343,14 @@ public static class PublishedContentExtensions
where T : class, IPublishedContent
=> content.Descendant(navigationQueryService, publishedStatusFilteringService, level, culture) as T;
/// <summary>
/// Gets the first descendant or self of the content item.
/// </summary>
/// <param name="content">The content item.</param>
/// <param name="navigationQueryService">The navigation query service.</param>
/// <param name="publishedStatusFilteringService">The published status filtering service.</param>
/// <param name="culture">The culture for variant content.</param>
/// <returns>The first descendant, or the content item itself if no descendants exist.</returns>
public static IPublishedContent DescendantOrSelf(
this IPublishedContent content,
INavigationQueryService navigationQueryService,
@@ -1143,6 +1364,15 @@ public static class PublishedContentExtensions
.FirstOrDefault() ??
content;
/// <summary>
/// Gets the first descendant or self of the content item at a specified level.
/// </summary>
/// <param name="content">The content item.</param>
/// <param name="navigationQueryService">The navigation query service.</param>
/// <param name="publishedStatusFilteringService">The published status filtering service.</param>
/// <param name="level">The level to find a descendant at.</param>
/// <param name="culture">The culture for variant content.</param>
/// <returns>The first descendant or self at the specified level, or null if none exists.</returns>
public static IPublishedContent? DescendantOrSelf(
this IPublishedContent content,
INavigationQueryService navigationQueryService,
@@ -1153,6 +1383,15 @@ public static class PublishedContentExtensions
.EnumerateDescendants(navigationQueryService, publishedStatusFilteringService, true, culture)
.FirstOrDefault(x => x.Level == level);
/// <summary>
/// Gets the first descendant or self of the content item of a specific content type.
/// </summary>
/// <param name="content">The content item.</param>
/// <param name="navigationQueryService">The navigation query service.</param>
/// <param name="publishedStatusFilteringService">The published status filtering service.</param>
/// <param name="contentTypeAlias">The content type alias to filter by.</param>
/// <param name="culture">The culture for variant content.</param>
/// <returns>The first descendant or self of the specified type, or null if none exists.</returns>
public static IPublishedContent? DescendantOrSelfOfType(
this IPublishedContent content,
INavigationQueryService navigationQueryService,
@@ -1168,6 +1407,15 @@ public static class PublishedContentExtensions
contentTypeAlias)
.FirstOrDefault();
/// <summary>
/// Gets the first descendant or self of the content item of a specific type.
/// </summary>
/// <typeparam name="T">The type of content to return.</typeparam>
/// <param name="content">The content item.</param>
/// <param name="navigationQueryService">The navigation query service.</param>
/// <param name="publishedStatusFilteringService">The published status filtering service.</param>
/// <param name="culture">The culture for variant content.</param>
/// <returns>The first descendant or self of the specified type, or null if none exists.</returns>
public static T? DescendantOrSelf<T>(
this IPublishedContent content,
INavigationQueryService navigationQueryService,
@@ -1178,6 +1426,16 @@ public static class PublishedContentExtensions
.EnumerateDescendants(navigationQueryService, publishedStatusFilteringService, true, culture)
.FirstOrDefault(x => x is T) as T;
/// <summary>
/// Gets the first descendant or self of the content item of a specific type at a specified level.
/// </summary>
/// <typeparam name="T">The type of content to return.</typeparam>
/// <param name="content">The content item.</param>
/// <param name="navigationQueryService">The navigation query service.</param>
/// <param name="publishedStatusFilteringService">The published status filtering service.</param>
/// <param name="level">The level to find a descendant at.</param>
/// <param name="culture">The culture for variant content.</param>
/// <returns>The first descendant or self of the specified type at the level, or null if none exists.</returns>
public static T? DescendantOrSelf<T>(
this IPublishedContent content,
INavigationQueryService navigationQueryService,
@@ -1225,13 +1483,12 @@ public static class PublishedContentExtensions
/// Gets the children of the content item.
/// </summary>
/// <param name="content">The content item.</param>
/// <param name="navigationQueryService"></param>
/// <param name="publishedStatusFilteringService"></param>
/// <param name="navigationQueryService">The navigation query service.</param>
/// <param name="publishedStatusFilteringService">The published status filtering service.</param>
/// <param name="culture">
/// The specific culture to get the URL children for. Default is null which will use the current culture in
/// <see cref="VariationContext" />
/// </param>
/// <param name="publishedCache"></param>
/// <remarks>
/// <para>Gets children that are available for the specified culture.</para>
/// <para>Children are sorted by their sortOrder.</para>
@@ -1260,14 +1517,13 @@ public static class PublishedContentExtensions
/// Gets the children of the content, filtered by a predicate.
/// </summary>
/// <param name="content">The content.</param>
/// <param name="navigationQueryService"></param>
/// <param name="publishedStatusFilteringService"></param>
/// <param name="navigationQueryService">The navigation query service.</param>
/// <param name="publishedStatusFilteringService">The published status filtering service.</param>
/// <param name="predicate">The predicate.</param>
/// <param name="culture">
/// The specific culture to filter for. If null is used the current culture is used. (Default is
/// null)
/// </param>
/// <param name="publishedCache"></param>
/// <returns>The children of the content, filtered by the predicate.</returns>
/// <remarks>
/// <para>Children are sorted by their sortOrder.</para>
@@ -1307,14 +1563,12 @@ public static class PublishedContentExtensions
/// </summary>
/// <typeparam name="T">The content type.</typeparam>
/// <param name="content">The content.</param>
/// <param name="variationContextAccessor">The accessor for the VariationContext</param>
/// <param name="navigationQueryService"></param>
/// <param name="publishStatusQueryService"></param>
/// <param name="navigationQueryService">The query service for the in-memory navigation structure.</param>
/// <param name="publishedStatusFilteringService">The service for filtering published content by status.</param>
/// <param name="culture">
/// The specific culture to filter for. If null is used the current culture is used. (Default is
/// null)
/// </param>
/// <param name="publishedCache"></param>
/// <returns>The children of content, of the given content type.</returns>
/// <remarks>
/// <para>Children are sorted by their sortOrder.</para>
@@ -1327,6 +1581,14 @@ public static class PublishedContentExtensions
where T : class, IPublishedContent
=> content.Children(navigationQueryService, publishedStatusFilteringService, culture).OfType<T>();
/// <summary>
/// Gets the first child of the content item.
/// </summary>
/// <param name="content">The content item.</param>
/// <param name="navigationQueryService">The navigation query service.</param>
/// <param name="publishedStatusFilteringService">The published status filtering service.</param>
/// <param name="culture">The culture for variant content.</param>
/// <returns>The first child, or null if no children exist.</returns>
public static IPublishedContent? FirstChild(
this IPublishedContent content,
INavigationQueryService navigationQueryService,
@@ -1349,6 +1611,15 @@ public static class PublishedContentExtensions
.ChildrenOfType(navigationQueryService, publishedStatusFilteringService, contentTypeAlias, culture)
.FirstOrDefault();
/// <summary>
/// Gets the first child of the content item that matches a predicate.
/// </summary>
/// <param name="content">The content item.</param>
/// <param name="navigationQueryService">The navigation query service.</param>
/// <param name="publishedStatusFilteringService">The published status filtering service.</param>
/// <param name="predicate">The predicate to filter children.</param>
/// <param name="culture">The culture for variant content.</param>
/// <returns>The first child matching the predicate, or null if none exists.</returns>
public static IPublishedContent? FirstChild(
this IPublishedContent content,
INavigationQueryService navigationQueryService,
@@ -1359,6 +1630,15 @@ public static class PublishedContentExtensions
.Children(navigationQueryService, publishedStatusFilteringService, predicate, culture)
.FirstOrDefault();
/// <summary>
/// Gets the first child of the content item with a specific unique identifier.
/// </summary>
/// <param name="content">The content item.</param>
/// <param name="navigationQueryService">The navigation query service.</param>
/// <param name="publishedStatusFilteringService">The published status filtering service.</param>
/// <param name="uniqueId">The unique identifier of the child to find.</param>
/// <param name="culture">The culture for variant content.</param>
/// <returns>The first child with the specified unique identifier, or null if none exists.</returns>
public static IPublishedContent? FirstChild(
this IPublishedContent content,
INavigationQueryService navigationQueryService,
@@ -1369,6 +1649,15 @@ public static class PublishedContentExtensions
.Children(navigationQueryService, publishedStatusFilteringService, x => x.Key == uniqueId, culture)
.FirstOrDefault();
/// <summary>
/// Gets the first child of the content item of a specific type.
/// </summary>
/// <typeparam name="T">The type of content to return.</typeparam>
/// <param name="content">The content item.</param>
/// <param name="navigationQueryService">The navigation query service.</param>
/// <param name="publishedStatusFilteringService">The published status filtering service.</param>
/// <param name="culture">The culture for variant content.</param>
/// <returns>The first child of the specified type, or null if none exists.</returns>
public static T? FirstChild<T>(
this IPublishedContent content,
INavigationQueryService navigationQueryService,
@@ -1379,6 +1668,16 @@ public static class PublishedContentExtensions
.Children<T>(navigationQueryService, publishedStatusFilteringService, culture)
.FirstOrDefault();
/// <summary>
/// Gets the first child of the content item of a specific type that matches a predicate.
/// </summary>
/// <typeparam name="T">The type of content to return.</typeparam>
/// <param name="content">The content item.</param>
/// <param name="navigationQueryService">The navigation query service.</param>
/// <param name="publishedStatusFilteringService">The published status filtering service.</param>
/// <param name="predicate">The predicate to filter children.</param>
/// <param name="culture">The culture for variant content.</param>
/// <returns>The first child of the specified type matching the predicate, or null if none exists.</returns>
public static T? FirstChild<T>(
this IPublishedContent content,
INavigationQueryService navigationQueryService,
@@ -1579,12 +1878,24 @@ public static class PublishedContentExtensions
#region Writer and creator
/// <summary>
/// Gets the name of the user who created the content item.
/// </summary>
/// <param name="content">The content item.</param>
/// <param name="userService">The user service.</param>
/// <returns>The name of the creator, or an empty string if not found.</returns>
public static string GetCreatorName(this IPublishedContent content, IUserService userService)
{
IProfile? user = userService.GetProfileById(content.CreatorId);
return user?.Name ?? string.Empty;
}
/// <summary>
/// Gets the name of the user who last updated the content item.
/// </summary>
/// <param name="content">The content item.</param>
/// <param name="userService">The user service.</param>
/// <returns>The name of the writer, or an empty string if not found.</returns>
public static string GetWriterName(this IPublishedContent content, IUserService userService)
{
IProfile? user = userService.GetProfileById(content.WriterId);
@@ -1642,70 +1953,190 @@ public static class PublishedContentExtensions
#endregion
#region Convenience overloads (using service locator)
/// <summary>
/// Gets an ancestor of the content item up to a maximum level.
/// </summary>
/// <param name="content">The content item.</param>
/// <param name="maxLevel">The maximum level to traverse.</param>
/// <returns>The ancestor at or below the maximum level, or null if not found.</returns>
public static IPublishedContent? Ancestor(this IPublishedContent content, int maxLevel)
=> content.Ancestor(GetNavigationQueryService(content), GetPublishedStatusFilteringService(content), maxLevel);
/// <summary>
/// Gets an ancestor of the content item of a specific content type.
/// </summary>
/// <param name="content">The content item.</param>
/// <param name="contentTypeAlias">The content type alias to find.</param>
/// <returns>The first ancestor of the specified type, or null if not found.</returns>
public static IPublishedContent? Ancestor(this IPublishedContent content, string contentTypeAlias)
=> content.Ancestor(GetNavigationQueryService(content), GetPublishedStatusFilteringService(content), contentTypeAlias);
/// <summary>
/// Gets an ancestor of the content item of a specific type up to a maximum level.
/// </summary>
/// <typeparam name="T">The type of content to return.</typeparam>
/// <param name="content">The content item.</param>
/// <param name="maxLevel">The maximum level to traverse.</param>
/// <returns>The ancestor of the specified type at or below the maximum level, or null if not found.</returns>
public static T? Ancestor<T>(this IPublishedContent content, int maxLevel)
where T : class, IPublishedContent
=> content.Ancestor<T>(GetNavigationQueryService(content), GetPublishedStatusFilteringService(content), maxLevel);
/// <summary>
/// Gets the ancestors of the content item up to a maximum level.
/// </summary>
/// <param name="content">The content item.</param>
/// <param name="maxLevel">The maximum level to traverse.</param>
/// <returns>An enumerable of ancestors at or below the maximum level.</returns>
public static IEnumerable<IPublishedContent> Ancestors(this IPublishedContent content, int maxLevel)
=> content.Ancestors(GetNavigationQueryService(content), GetPublishedStatusFilteringService(content), maxLevel);
/// <summary>
/// Gets the ancestors of the content item of a specific content type.
/// </summary>
/// <param name="content">The content item.</param>
/// <param name="contentTypeAlias">The content type alias to filter by.</param>
/// <returns>An enumerable of ancestors of the specified type.</returns>
public static IEnumerable<IPublishedContent> Ancestors(this IPublishedContent content, string contentTypeAlias)
=> content.Ancestors(GetNavigationQueryService(content), GetPublishedStatusFilteringService(content), contentTypeAlias);
/// <summary>
/// Gets the ancestors of the content item of a specific type.
/// </summary>
/// <typeparam name="T">The type of content to return.</typeparam>
/// <param name="content">The content item.</param>
/// <returns>An enumerable of ancestors of the specified type.</returns>
public static IEnumerable<T> Ancestors<T>(this IPublishedContent content)
where T : class, IPublishedContent
=> content.Ancestors<T>(GetNavigationQueryService(content), GetPublishedStatusFilteringService(content));
/// <summary>
/// Gets the ancestors of the content item of a specific type up to a maximum level.
/// </summary>
/// <typeparam name="T">The type of content to return.</typeparam>
/// <param name="content">The content item.</param>
/// <param name="maxLevel">The maximum level to traverse.</param>
/// <returns>An enumerable of ancestors of the specified type at or below the maximum level.</returns>
public static IEnumerable<T> Ancestors<T>(this IPublishedContent content, int maxLevel)
where T : class, IPublishedContent
=> content.Ancestors<T>(GetNavigationQueryService(content), GetPublishedStatusFilteringService(content), maxLevel);
/// <summary>
/// Gets an ancestor or self of the content item at or below a maximum level.
/// </summary>
/// <param name="content">The content item.</param>
/// <param name="maxLevel">The maximum level to traverse.</param>
/// <returns>The content item or an ancestor at or below the maximum level.</returns>
public static IPublishedContent AncestorOrSelf(this IPublishedContent content, int maxLevel)
=> content.AncestorOrSelf(GetNavigationQueryService(content), GetPublishedStatusFilteringService(content), maxLevel);
/// <summary>
/// Gets an ancestor or self of the content item of a specific content type.
/// </summary>
/// <param name="content">The content item.</param>
/// <param name="contentTypeAlias">The content type alias to find.</param>
/// <returns>The content item or an ancestor of the specified type.</returns>
public static IPublishedContent AncestorOrSelf(this IPublishedContent content, string contentTypeAlias)
=> content.AncestorOrSelf(GetNavigationQueryService(content), GetPublishedStatusFilteringService(content), contentTypeAlias);
/// <summary>
/// Gets an ancestor or self of the content item of a specific type up to a maximum level.
/// </summary>
/// <typeparam name="T">The type of content to return.</typeparam>
/// <param name="content">The content item.</param>
/// <param name="maxLevel">The maximum level to traverse.</param>
/// <returns>The content item or an ancestor of the specified type at or below the maximum level, or null if not found.</returns>
public static T? AncestorOrSelf<T>(this IPublishedContent content, int maxLevel)
where T : class, IPublishedContent
=> content.AncestorOrSelf<T>(GetNavigationQueryService(content), GetPublishedStatusFilteringService(content), maxLevel);
/// <summary>
/// Gets the ancestors or self of the content item up to a maximum level.
/// </summary>
/// <param name="content">The content item.</param>
/// <param name="maxLevel">The maximum level to traverse.</param>
/// <returns>An enumerable of the content item and its ancestors at or below the maximum level.</returns>
public static IEnumerable<IPublishedContent> AncestorsOrSelf(this IPublishedContent content, int maxLevel)
=> content.AncestorsOrSelf(GetNavigationQueryService(content), GetPublishedStatusFilteringService(content), maxLevel);
/// <summary>
/// Gets the ancestors or self of the content item of a specific content type.
/// </summary>
/// <param name="content">The content item.</param>
/// <param name="contentTypeAlias">The content type alias to filter by.</param>
/// <returns>An enumerable of the content item and its ancestors of the specified type.</returns>
public static IEnumerable<IPublishedContent> AncestorsOrSelf(this IPublishedContent content, string contentTypeAlias)
=> content.Ancestors(GetNavigationQueryService(content), GetPublishedStatusFilteringService(content), contentTypeAlias);
/// <summary>
/// Gets the ancestors or self of the content item of a specific type up to a maximum level.
/// </summary>
/// <typeparam name="T">The type of content to return.</typeparam>
/// <param name="content">The content item.</param>
/// <param name="maxLevel">The maximum level to traverse.</param>
/// <returns>An enumerable of the content item and its ancestors of the specified type at or below the maximum level.</returns>
public static IEnumerable<T> AncestorsOrSelf<T>(this IPublishedContent content, int maxLevel)
where T : class, IPublishedContent
=> content.AncestorsOrSelf<T>(GetNavigationQueryService(content), GetPublishedStatusFilteringService(content), maxLevel);
/// <summary>
/// Gets the ancestors or self of the content item, optionally filtered by a predicate.
/// </summary>
/// <param name="content">The content item.</param>
/// <param name="orSelf">Whether to include the content item itself.</param>
/// <param name="func">An optional predicate to filter the ancestors.</param>
/// <returns>An enumerable of the content item and/or its ancestors matching the criteria.</returns>
public static IEnumerable<IPublishedContent> AncestorsOrSelf(this IPublishedContent content, bool orSelf, Func<IPublishedContent, bool>? func)
=> content.AncestorsOrSelf(GetNavigationQueryService(content), GetPublishedStatusFilteringService(content), orSelf, func);
/// <summary>
/// Gets the breadcrumbs (ancestors and self, top to bottom) for the content item.
/// </summary>
/// <param name="content">The content item.</param>
/// <param name="andSelf">Whether to include the content item itself. Default is true.</param>
/// <returns>An enumerable of the breadcrumb trail from root to the content item.</returns>
public static IEnumerable<IPublishedContent> Breadcrumbs(
this IPublishedContent content,
bool andSelf = true) =>
content.Breadcrumbs(GetNavigationQueryService(content), GetPublishedStatusFilteringService(content), andSelf);
/// <summary>
/// Gets the breadcrumbs (ancestors and self, top to bottom) for the content item at or above a minimum level.
/// </summary>
/// <param name="content">The content item.</param>
/// <param name="minLevel">The minimum level to include in the breadcrumbs.</param>
/// <param name="andSelf">Whether to include the content item itself. Default is true.</param>
/// <returns>An enumerable of the breadcrumb trail from the minimum level to the content item.</returns>
public static IEnumerable<IPublishedContent> Breadcrumbs(
this IPublishedContent content,
int minLevel,
bool andSelf = true) =>
content.Breadcrumbs(GetNavigationQueryService(content), GetPublishedStatusFilteringService(content), minLevel, andSelf);
/// <summary>
/// Gets the breadcrumbs (ancestors and self, top to bottom) for the content item of a specific type.
/// </summary>
/// <typeparam name="T">The type of content to include in the breadcrumbs.</typeparam>
/// <param name="content">The content item.</param>
/// <param name="andSelf">Whether to include the content item itself. Default is true.</param>
/// <returns>An enumerable of the breadcrumb trail of the specified type.</returns>
public static IEnumerable<IPublishedContent> Breadcrumbs<T>(
this IPublishedContent content,
bool andSelf = true)
where T : class, IPublishedContent=>
where T : class, IPublishedContent =>
content.Breadcrumbs<T>(GetNavigationQueryService(content), GetPublishedStatusFilteringService(content), andSelf);
/// <summary>
/// Gets all descendants or self of a collection of content items of a specific content type.
/// </summary>
/// <param name="parentNodes">The collection of parent content items.</param>
/// <param name="navigationQueryService">The navigation query service.</param>
/// <param name="publishedStatusFilteringService">The published status filtering service.</param>
/// <param name="docTypeAlias">The content type alias to filter by.</param>
/// <param name="culture">The culture for variant content.</param>
/// <returns>An enumerable of all content items and their descendants of the specified type.</returns>
public static IEnumerable<IPublishedContent> DescendantsOrSelfOfType(
this IEnumerable<IPublishedContent> parentNodes,
INavigationQueryService navigationQueryService,
@@ -1718,6 +2149,15 @@ public static class PublishedContentExtensions
docTypeAlias,
culture));
/// <summary>
/// Gets all descendants or self of a collection of content items of a specific type.
/// </summary>
/// <typeparam name="T">The type of content to return.</typeparam>
/// <param name="parentNodes">The collection of parent content items.</param>
/// <param name="navigationQueryService">The navigation query service.</param>
/// <param name="publishedStatusFilteringService">The published status filtering service.</param>
/// <param name="culture">The culture for variant content.</param>
/// <returns>An enumerable of all content items and their descendants of the specified type.</returns>
public static IEnumerable<T> DescendantsOrSelf<T>(
this IEnumerable<IPublishedContent> parentNodes,
INavigationQueryService navigationQueryService,
@@ -1729,6 +2169,8 @@ public static class PublishedContentExtensions
publishedStatusFilteringService,
culture));
#endregion
private static INavigationQueryService GetNavigationQueryService(IPublishedContent content)
{
switch (content.ItemType)
@@ -758,13 +758,7 @@ public static class StringExtensions
/// <returns>The hashed string</returns>
private static string GenerateHash(this string str, string? hashType)
{
HashAlgorithm? hasher = null;
// create an instance of the correct hashing provider based on the type passed in
if (hashType is not null)
{
hasher = HashAlgorithm.Create(hashType);
}
HashAlgorithm? hasher = CreateHashAlgorithm(hashType);
if (hasher == null)
{
@@ -794,6 +788,33 @@ public static class StringExtensions
}
}
/// <summary>
/// Creates a hash algorithm instance by name.
/// </summary>
/// <param name="algorithmName">The algorithm name (e.g., "SHA1", "SHA256", "MD5").</param>
/// <returns>A HashAlgorithm instance, or null if the algorithm is not recognized.</returns>
private static HashAlgorithm? CreateHashAlgorithm(string? algorithmName)
{
if (string.IsNullOrEmpty(algorithmName))
{
return null;
}
return algorithmName.ToUpperInvariant() switch
{
"SHA1" or "SHA-1" or "SYSTEM.SECURITY.CRYPTOGRAPHY.SHA1" => SHA1.Create(),
"SHA256" or "SHA-256" or "SYSTEM.SECURITY.CRYPTOGRAPHY.SHA256" => SHA256.Create(),
"SHA384" or "SHA-384" or "SYSTEM.SECURITY.CRYPTOGRAPHY.SHA384" => SHA384.Create(),
"SHA512" or "SHA-512" or "SYSTEM.SECURITY.CRYPTOGRAPHY.SHA512" => SHA512.Create(),
"MD5" or "SYSTEM.SECURITY.CRYPTOGRAPHY.MD5" => MD5.Create(),
"HMACSHA1" or "SYSTEM.SECURITY.CRYPTOGRAPHY.HMACSHA1" => new HMACSHA1(),
"HMACSHA256" or "SYSTEM.SECURITY.CRYPTOGRAPHY.HMACSHA256" => new HMACSHA256(),
"HMACSHA384" or "SYSTEM.SECURITY.CRYPTOGRAPHY.HMACSHA384" => new HMACSHA384(),
"HMACSHA512" or "SYSTEM.SECURITY.CRYPTOGRAPHY.HMACSHA512" => new HMACSHA512(),
_ => null
};
}
/// <summary>
/// Encodes a string so that it is 'safe' for URLs, files, etc..
/// </summary>
+57 -6
View File
@@ -16,16 +16,39 @@ public class HashGenerator : DisposableObjectSlim
private readonly MemoryStream _ms = new();
private StreamWriter _writer;
/// <summary>
/// Initializes a new instance of the <see cref="HashGenerator"/> class.
/// </summary>
public HashGenerator() => _writer = new StreamWriter(_ms, Encoding.Unicode, 1024, true);
/// <summary>
/// Adds an integer value to the hash computation.
/// </summary>
/// <param name="i">The integer value to add.</param>
public void AddInt(int i) => _writer.Write(i);
/// <summary>
/// Adds a long value to the hash computation.
/// </summary>
/// <param name="i">The long value to add.</param>
public void AddLong(long i) => _writer.Write(i);
/// <summary>
/// Adds an object's string representation to the hash computation.
/// </summary>
/// <param name="o">The object to add.</param>
public void AddObject(object o) => _writer.Write(o);
/// <summary>
/// Adds a DateTime value to the hash computation using its ticks.
/// </summary>
/// <param name="d">The DateTime value to add.</param>
public void AddDateTime(DateTime d) => _writer.Write(d.Ticks);
/// <summary>
/// Adds a string to the hash computation.
/// </summary>
/// <param name="s">The string to add. If null, nothing is added.</param>
public void AddString(string s)
{
if (s != null)
@@ -34,6 +57,14 @@ public class HashGenerator : DisposableObjectSlim
}
}
/// <summary>
/// Adds a string to the hash computation in a case-insensitive manner.
/// </summary>
/// <param name="s">The string to add. If null, nothing is added.</param>
/// <remarks>
/// The string is converted to uppercase before being added to ensure
/// case-insensitive hash generation.
/// </remarks>
public void AddCaseInsensitiveString(string s)
{
// I've tried to no allocate a new string with this which can be done if we use the CompareInfo.GetSortKey method which will create a new
@@ -48,6 +79,14 @@ public class HashGenerator : DisposableObjectSlim
}
}
/// <summary>
/// Adds a file system item (file or directory) to the hash computation.
/// </summary>
/// <param name="f">The file system item to add.</param>
/// <remarks>
/// For files, adds the full name, creation time, last write time, and length.
/// For directories, recursively adds all files and subdirectories.
/// </remarks>
public void AddFileSystemItem(FileSystemInfo f)
{
// if it doesn't exist, don't proceed.
@@ -80,14 +119,27 @@ public class HashGenerator : DisposableObjectSlim
}
}
/// <summary>
/// Adds a file to the hash computation.
/// </summary>
/// <param name="f">The file to add.</param>
public void AddFile(FileInfo f) => AddFileSystemItem(f);
/// <summary>
/// Adds a folder and its contents to the hash computation.
/// </summary>
/// <param name="d">The directory to add.</param>
public void AddFolder(DirectoryInfo d) => AddFileSystemItem(d);
/// <summary>
/// Returns the generated hash output of all added objects
/// Returns the generated hash of all added objects.
/// </summary>
/// <returns></returns>
/// <returns>A hexadecimal string representation of the computed hash.</returns>
/// <remarks>
/// Uses SHA1 when FIPS compliance is required, otherwise uses MD5.
/// This method can be called multiple times; subsequent calls will include
/// any objects added after the previous call.
/// </remarks>
public string GenerateHash()
{
// flush,close,dispose the writer,then create a new one since it's possible to keep adding after GenerateHash is called.
@@ -96,10 +148,8 @@ public class HashGenerator : DisposableObjectSlim
_writer.Dispose();
_writer = new StreamWriter(_ms, Encoding.UTF8, 1024, true);
var hashType = CryptoConfig.AllowOnlyFipsAlgorithms ? "SHA1" : "MD5";
// create an instance of the correct hashing provider based on the type passed in
HashAlgorithm hasher = HashAlgorithm.Create(hashType) ?? throw new InvalidOperationException("No hashing type found by name " + hashType);
// Use SHA1 for FIPS compliance, otherwise MD5
HashAlgorithm hasher = CryptoConfig.AllowOnlyFipsAlgorithms ? SHA1.Create() : MD5.Create();
using (hasher)
{
@@ -123,6 +173,7 @@ public class HashGenerator : DisposableObjectSlim
}
}
/// <inheritdoc />
protected override void DisposeResources()
{
_writer.Close();
+1 -1
View File
@@ -44,7 +44,7 @@ public class LogProfiler : IProfiler
private readonly Action<long> _callback;
private readonly Stopwatch _stopwatch = Stopwatch.StartNew();
protected internal LightDisposableTimer(Action<long> callback)
internal LightDisposableTimer(Action<long> callback)
{
_callback = callback ?? throw new ArgumentNullException(nameof(callback));
}
@@ -19,7 +19,7 @@ public class BlockGridModel : BlockModelCollection<BlockGridItem>
/// <value>
/// The empty <see cref="BlockGridModel" />.
/// </value>
public static BlockGridModel Empty { get; } = new BlockGridModel();
public static new BlockGridModel Empty { get; } = new BlockGridModel();
/// <summary>
/// Prevents a default instance of the <see cref="BlockGridModel" /> class from being created.
@@ -34,5 +34,5 @@ public class BlockListModel : BlockModelCollection<BlockListItem>
/// <value>
/// The empty <see cref="BlockListModel" />.
/// </value>
public static BlockListModel Empty { get; } = new();
public static new BlockListModel Empty { get; } = new();
}
@@ -34,5 +34,5 @@ public class RichTextBlockModel : BlockModelCollection<RichTextBlockItem>
/// <value>
/// The empty <see cref="RichTextBlockModel" />.
/// </value>
public static RichTextBlockModel Empty { get; } = new();
public static new RichTextBlockModel Empty { get; } = new();
}
+3 -1
View File
@@ -145,7 +145,9 @@ public sealed class CultureImpact
/// <param name="savingCultures"></param>
/// <param name="defaultCulture"></param>
/// <returns></returns>
public static string? GetCultureForInvariantErrors(IContent? content, string?[] savingCultures,
public static string? GetCultureForInvariantErrors(
IContent? content,
string?[] savingCultures,
string? defaultCulture)
{
if (content == null)
+4 -4
View File
@@ -50,11 +50,11 @@ public interface IContentType : IContentTypeComposition
bool RemoveTemplate(ITemplate template);
/// <summary>
/// Creates a deep clone of the current entity with its identity/alias and it's property identities reset
/// Creates a deep clone of the current entity with its identity/alias and it's property identities reset.
/// </summary>
/// <param name="newAlias"></param>
/// <returns></returns>
IContentType DeepCloneWithResetIdentities(string newAlias);
/// <param name="newAlias">The new alias for the cloned content type.</param>
/// <returns>A deep clone of the current <see cref="IContentType"/> with reset identities.</returns>
new IContentType DeepCloneWithResetIdentities(string newAlias);
/// <summary>
/// Gets or sets the history cleanup configuration.
+5 -5
View File
@@ -1,14 +1,14 @@
namespace Umbraco.Cms.Core.Models;
/// <summary>
/// Defines a ContentType, which Media is based on
/// Defines a ContentType, which Media is based on.
/// </summary>
public interface IMediaType : IContentTypeComposition
{
/// <summary>
/// Creates a deep clone of the current entity with its identity/alias and it's property identities reset
/// Creates a deep clone of the current entity with its identity/alias and it's property identities reset.
/// </summary>
/// <param name="newAlias"></param>
/// <returns></returns>
IMediaType DeepCloneWithResetIdentities(string newAlias);
/// <param name="newAlias">The new alias for the cloned media type.</param>
/// <returns>A deep clone of the current <see cref="IMediaType"/> with reset identities.</returns>
public new IMediaType DeepCloneWithResetIdentities(string newAlias);
}
+2 -2
View File
@@ -513,8 +513,8 @@ public class Member : ContentBase, IMember
{
StaticApplicationLogging.Logger.LogWarning(
"Trying to access the '{PropertyName}' property on '{MemberType}' " +
"but the {PropertyAlias} property does not exist on the member type so a default value is returned. " +
"Ensure that you have a property type with alias: {PropertyAlias} configured on your member type in order to use the '{PropertyName}' property on the model correctly.",
"but the property type with alias '{PropertyAlias}' does not exist on the member type. " +
"A default value is returned. Ensure this property type is configured on your member type.",
logPropertyName,
typeof(Member),
logPropertyAlias);
@@ -9,6 +9,11 @@ public interface IReadOnlyUserGroup
{
string? Name { get; }
string Alias { get; }
// TODO (V18): Remove the default implementations.
string? Description { get { return null; } }
string? Icon { get; }
int Id { get; }
@@ -19,11 +24,6 @@ public interface IReadOnlyUserGroup
int? StartMediaId { get; }
/// <summary>
/// The alias
/// </summary>
string Alias { get; }
// This is set to return true as default to avoid breaking changes.
bool HasAccessToAllLanguages => true;
@@ -4,12 +4,24 @@ using Umbraco.Cms.Core.Models.Membership.Permissions;
namespace Umbraco.Cms.Core.Models.Membership;
/// <summary>
/// Represents a user group in Umbraco.
/// </summary>
public interface IUserGroup : IEntity, IRememberBeingDirty
{
/// <summary>
/// Gets or sets the alias of the user group.
/// </summary>
string Alias { get; set; }
/// <summary>
/// Gets or sets the starting content node ID for this user group.
/// </summary>
int? StartContentId { get; set; }
/// <summary>
/// Gets or sets the starting media node ID for this user group.
/// </summary>
int? StartMediaId { get; set; }
/// <summary>
@@ -22,6 +34,18 @@ public interface IUserGroup : IEntity, IRememberBeingDirty
/// </summary>
string? Name { get; set; }
/// <summary>
/// Gets or sets the description of the user group.
/// </summary>
/// <remarks>
/// TODO (V18): Remove the default implementations.
/// </remarks>
string? Description
{
get => null;
set { }
}
/// <summary>
/// If this property is true it will give the group access to all languages
/// </summary>
@@ -40,26 +64,57 @@ public interface IUserGroup : IEntity, IRememberBeingDirty
/// </remarks>
ISet<string> Permissions { get; set; }
/// <summary>
/// Gets or sets the granular permissions for this user group.
/// </summary>
ISet<IGranularPermission> GranularPermissions { get; set; }
/// <summary>
/// Gets the collection of section aliases that this user group has access to.
/// </summary>
IEnumerable<string> AllowedSections { get; }
/// <summary>
/// Removes access to a section for this user group.
/// </summary>
/// <param name="sectionAlias">The alias of the section to remove.</param>
void RemoveAllowedSection(string sectionAlias);
/// <summary>
/// Adds access to a section for this user group.
/// </summary>
/// <param name="sectionAlias">The alias of the section to add.</param>
void AddAllowedSection(string sectionAlias);
/// <summary>
/// Removes access to all sections for this user group.
/// </summary>
void ClearAllowedSections();
/// <summary>
/// Gets the collection of language IDs that this user group has access to.
/// </summary>
IEnumerable<int> AllowedLanguages => Enumerable.Empty<int>();
/// <summary>
/// Removes access to a language for this user group.
/// </summary>
/// <param name="languageId">The ID of the language to remove.</param>
void RemoveAllowedLanguage(int languageId)
{
}
/// <summary>
/// Adds access to a language for this user group.
/// </summary>
/// <param name="languageId">The ID of the language to add.</param>
void AddAllowedLanguage(int languageId)
{
}
/// <summary>
/// Removes access to all languages for this user group.
/// </summary>
void ClearAllowedLanguages()
{
}
@@ -4,6 +4,7 @@ namespace Umbraco.Cms.Core.Models.Membership;
public class ReadOnlyUserGroup : IReadOnlyUserGroup, IEquatable<ReadOnlyUserGroup>
{
[Obsolete("Please use the constructor that includes all parameters. Scheduled for removal in Umbraco 19.")]
public ReadOnlyUserGroup(
int id,
Guid key,
@@ -17,11 +18,43 @@ public class ReadOnlyUserGroup : IReadOnlyUserGroup, IEquatable<ReadOnlyUserGrou
ISet<string> permissions,
ISet<IGranularPermission> granularPermissions,
bool hasAccessToAllLanguages)
: this(
id,
key,
name,
null,
icon,
startContentId,
startMediaId,
alias,
allowedLanguages,
allowedSections,
permissions,
granularPermissions,
hasAccessToAllLanguages)
{
}
public ReadOnlyUserGroup(
int id,
Guid key,
string? name,
string? description,
string? icon,
int? startContentId,
int? startMediaId,
string? alias,
IEnumerable<int> allowedLanguages,
IEnumerable<string> allowedSections,
ISet<string> permissions,
ISet<IGranularPermission> granularPermissions,
bool hasAccessToAllLanguages)
{
Name = name ?? string.Empty;
Icon = icon;
Id = id;
Key = key;
Name = name ?? string.Empty;
Description = description;
Icon = icon;
Alias = alias ?? string.Empty;
AllowedLanguages = allowedLanguages.ToArray();
AllowedSections = allowedSections.ToArray();
@@ -38,23 +71,10 @@ public class ReadOnlyUserGroup : IReadOnlyUserGroup, IEquatable<ReadOnlyUserGrou
public Guid Key { get; }
public bool Equals(ReadOnlyUserGroup? other)
{
if (ReferenceEquals(null, other))
{
return false;
}
if (ReferenceEquals(this, other))
{
return true;
}
return string.Equals(Alias, other.Alias);
}
public string Name { get; }
public string? Description { get; }
public string? Icon { get; }
public int? StartContentId { get; }
@@ -77,7 +97,7 @@ public class ReadOnlyUserGroup : IReadOnlyUserGroup, IEquatable<ReadOnlyUserGrou
public override bool Equals(object? obj)
{
if (ReferenceEquals(null, obj))
if (obj is null)
{
return false;
}
@@ -95,6 +115,21 @@ public class ReadOnlyUserGroup : IReadOnlyUserGroup, IEquatable<ReadOnlyUserGrou
return Equals((ReadOnlyUserGroup)obj);
}
public bool Equals(ReadOnlyUserGroup? other)
{
if (other is null)
{
return false;
}
if (ReferenceEquals(this, other))
{
return true;
}
return string.Equals(Alias, other.Alias);
}
public override int GetHashCode() => Alias?.GetHashCode() ?? base.GetHashCode();
public static bool operator !=(ReadOnlyUserGroup left, ReadOnlyUserGroup right) => !Equals(left, right);
@@ -30,6 +30,7 @@ public class UserGroup : EntityBase, IUserGroup, IReadOnlyUserGroup
private string _alias;
private string? _icon;
private string _name;
private string? _description;
private bool _hasAccessToAllLanguages;
private ISet<string> _permissions;
private ISet<IGranularPermission> _granularPermissions;
@@ -53,13 +54,13 @@ public class UserGroup : EntityBase, IUserGroup, IReadOnlyUserGroup
}
/// <summary>
/// Constructor to create an existing user group
/// Constructor to create an existing user group.
/// </summary>
/// <param name="userCount"></param>
/// <param name="alias"></param>
/// <param name="name"></param>
/// <param name="icon"></param>
/// <param name="shortStringHelper"></param>
/// <param name="shortStringHelper">The short string helper.</param>
/// <param name="userCount">The user count.</param>
/// <param name="alias">The alias.</param>
/// <param name="name">The name.</param>
/// <param name="icon">The icon.</param>
public UserGroup(
IShortStringHelper shortStringHelper,
int userCount,
@@ -100,7 +101,8 @@ public class UserGroup : EntityBase, IUserGroup, IReadOnlyUserGroup
{
get => _alias;
set => SetPropertyValueAndDetectChanges(
value.ToCleanString(_shortStringHelper, CleanStringType.Alias | CleanStringType.UmbracoCase), ref _alias!,
value.ToCleanString(_shortStringHelper, CleanStringType.Alias | CleanStringType.UmbracoCase),
ref _alias!,
nameof(Alias));
}
@@ -111,6 +113,13 @@ public class UserGroup : EntityBase, IUserGroup, IReadOnlyUserGroup
set => SetPropertyValueAndDetectChanges(value, ref _name!, nameof(Name));
}
[DataMember]
public string? Description
{
get => _description;
set => SetPropertyValueAndDetectChanges(value, ref _description!, nameof(Description));
}
[DataMember]
public bool HasAccessToAllLanguages
{
@@ -18,6 +18,7 @@ public static class UserGroupExtensions
group.Id,
group.Key,
group.Name,
group.Description,
group.Icon,
group.StartContentId,
group.StartMediaId,
@@ -1,4 +1,4 @@
using System.Runtime.InteropServices;
using System.Collections.Concurrent;
namespace Umbraco.Cms.Core.Persistence.Repositories;
@@ -8,33 +8,25 @@ namespace Umbraco.Cms.Core.Persistence.Repositories;
public static class RepositoryCacheKeys
{
/// <summary>
/// A cache for the keys we don't keep allocating strings.
/// A thread-safe cache for the keys so we don't keep allocating strings.
/// </summary>
private static readonly Dictionary<Type, string> Keys = new();
private static readonly ConcurrentDictionary<Type, string> _keys = new();
/// <summary>
/// Gets the repository cache key for the provided type.
/// </summary>
/// <typeparam name="T">The entity type to get the cache key for.</typeparam>
/// <returns>A cache key string in the format "uRepo_{TypeName}_".</returns>
public static string GetKey<T>()
{
Type type = typeof(T);
// The following code is a micro-optimization to avoid an unnecessary lookup in the Keys dictionary, when writing the newly created key.
// Previously, the code was:
// return Keys.TryGetValue(type, out var key)
// ? key
// : Keys[type] = "uRepo_" + type.Name + "_";
// Look up the existing value or get a reference to the newly created default value.
ref string? key = ref CollectionsMarshal.GetValueRefOrAddDefault(Keys, type, out _);
// As we have the reference, we can just assign it if null, without the expensive write back to the dictionary.
return key ??= "uRepo_" + type.Name + "_";
}
=> _keys.GetOrAdd(typeof(T), static type => "uRepo_" + type.Name + "_");
/// <summary>
/// Gets the repository cache key for the provided type and Id.
/// </summary>
/// <typeparam name="T">The entity type to get the cache key for.</typeparam>
/// <typeparam name="TId">The type of the entity identifier.</typeparam>
/// <param name="id">The entity identifier.</param>
/// <returns>A cache key string in the format "uRepo_{TypeName}_{Id}", or an empty string if the id is the default value.</returns>
public static string GetKey<T, TId>(TId? id)
{
if (EqualityComparer<TId?>.Default.Equals(id, default))
@@ -9,8 +9,6 @@ namespace Umbraco.Cms.Core.PropertyEditors;
[DataContract]
public class ConfigurationField
{
private readonly string? _view;
/// <summary>
/// Initializes a new instance of the <see cref="ConfigurationField" /> class.
/// </summary>
@@ -22,6 +20,7 @@ public class ConfigurationField
/// <summary>
/// Initializes a new instance of the <see cref="ConfigurationField" /> class.
/// </summary>
/// <param name="validators">The validators for this configuration field.</param>
public ConfigurationField(params IValueValidator[] validators)
: this(validators.ToList())
{
@@ -8,8 +8,14 @@ namespace Umbraco.Cms.Core.PropertyEditors;
/// </summary>
public class DefaultPropertyIndexValueFactory : IPropertyIndexValueFactory
{
public IEnumerable<IndexValue> GetIndexValues(IProperty property, string? culture, string? segment, bool published,
IEnumerable<string> availableCultures, IDictionary<Guid, IContentType> contentTypeDictionary)
/// <inheritdoc />
public IEnumerable<IndexValue> GetIndexValues(
IProperty property,
string? culture,
string? segment,
bool published,
IEnumerable<string> availableCultures,
IDictionary<Guid, IContentType> contentTypeDictionary)
=>
[
new IndexValue
@@ -66,6 +66,17 @@ public interface IDataEditor : IDiscoverable
/// <returns>The result of the merge operation.</returns>
object? MergePartialPropertyValueForCulture(object? sourceValue, object? targetValue, string? culture) => sourceValue;
object? MergeVariantInvariantPropertyValue(object? sourceValue, object? targetValue,
bool canUpdateInvariantData, HashSet<string> allowedCultures) => sourceValue;
/// <summary>
/// Merges variant and invariant property values.
/// </summary>
/// <param name="sourceValue">The source property value.</param>
/// <param name="targetValue">The target property value.</param>
/// <param name="canUpdateInvariantData">A value indicating whether invariant data can be updated.</param>
/// <param name="allowedCultures">The set of allowed cultures.</param>
/// <returns>The result of the merge operation.</returns>
object? MergeVariantInvariantPropertyValue(
object? sourceValue,
object? targetValue,
bool canUpdateInvariantData,
HashSet<string> allowedCultures) => sourceValue;
}
@@ -8,7 +8,12 @@ namespace Umbraco.Cms.Core.PropertyEditors;
public class NoopPropertyIndexValueFactory : IPropertyIndexValueFactory
{
/// <inheritdoc />
public IEnumerable<IndexValue> GetIndexValues(IProperty property, string? culture, string? segment, bool published,
IEnumerable<string> availableCultures, IDictionary<Guid, IContentType> contentTypeDictionary)
public IEnumerable<IndexValue> GetIndexValues(
IProperty property,
string? culture,
string? segment,
bool published,
IEnumerable<string> availableCultures,
IDictionary<Guid, IContentType> contentTypeDictionary)
=> [];
}
@@ -72,7 +72,11 @@ public class TextOnlyValueEditor : DataValueEditor
_localizedTextService = localizedTextService;
}
public IEnumerable<ValidationResult> Validate(object? value, string? valueType, object? dataTypeConfiguration,
/// <inheritdoc />
public IEnumerable<ValidationResult> Validate(
object? value,
string? valueType,
object? dataTypeConfiguration,
PropertyValidationContext validationContext)
{
int? maxCharacters = dataTypeConfiguration switch
@@ -4,18 +4,21 @@ using Umbraco.Extensions;
namespace Umbraco.Cms.Core.PublishedCache;
/// <summary>
/// Represents a property of a published element with caching support for property value conversions.
/// </summary>
internal sealed class PublishedElementPropertyBase : PublishedPropertyBase
{
protected readonly IPublishedElement Element;
private readonly IPublishedElement _element;
// define constant - determines whether to use cache when previewing
// to store eg routes, property converted values, anything - caching
// means faster execution, but uses memory - not sure if we want it
// so making it configurable.
private readonly Lock _locko = new();
private readonly Lock _cacheLock = new();
private readonly object? _sourceValue;
protected readonly bool IsMember;
protected readonly bool IsPreviewing;
private readonly bool _isMember;
private readonly bool _isPreviewing;
private readonly VariationContext _variationContext;
private readonly ICacheManager? _cacheManager;
private CacheValues? _cacheValues;
@@ -24,6 +27,16 @@ internal sealed class PublishedElementPropertyBase : PublishedPropertyBase
private object? _interValue;
private string? _valuesCacheKey;
/// <summary>
/// Initializes a new instance of the <see cref="PublishedElementPropertyBase"/> class.
/// </summary>
/// <param name="propertyType">The published property type.</param>
/// <param name="element">The published element that owns this property.</param>
/// <param name="previewing">Whether this is a preview request.</param>
/// <param name="referenceCacheLevel">The reference cache level.</param>
/// <param name="variationContext">The variation context for culture and segment.</param>
/// <param name="cacheManager">The cache manager.</param>
/// <param name="sourceValue">The source value of the property.</param>
public PublishedElementPropertyBase(
IPublishedPropertyType propertyType,
IPublishedElement element,
@@ -35,11 +48,11 @@ internal sealed class PublishedElementPropertyBase : PublishedPropertyBase
: base(propertyType, referenceCacheLevel)
{
_sourceValue = sourceValue;
Element = element;
IsPreviewing = previewing;
_element = element;
_isPreviewing = previewing;
_variationContext = variationContext;
_cacheManager = cacheManager;
IsMember = propertyType.ContentType?.ItemType == PublishedItemType.Member;
_isMember = propertyType.ContentType?.ItemType == PublishedItemType.Member;
}
// used to cache the CacheValues of this property
@@ -47,9 +60,11 @@ internal sealed class PublishedElementPropertyBase : PublishedPropertyBase
private string ValuesCacheKey => _valuesCacheKey ??= PropertyCacheValuesKey();
private string PropertyCacheValuesKey() =>
$"PublishedSnapshot.Property.CacheValues[{(IsPreviewing ? "D:" : "P:")}{Element.Key}:{Alias}:{_variationContext.Culture.IfNullOrWhiteSpace("inv")}+{_variationContext.Segment.IfNullOrWhiteSpace("inv")}]";
$"PublishedSnapshot.Property.CacheValues[{(_isPreviewing ? "D:" : "P:")}{_element.Key}:{Alias}:{_variationContext.Culture.IfNullOrWhiteSpace("inv")}+{_variationContext.Segment.IfNullOrWhiteSpace("inv")}]";
// ReSharper restore InconsistentlySynchronizedField
/// <inheritdoc />
public override bool HasValue(string? culture = null, string? segment = null)
{
var hasValue = PropertyType.IsValue(_sourceValue, PropertyValueLevel.Source);
@@ -60,7 +75,7 @@ internal sealed class PublishedElementPropertyBase : PublishedPropertyBase
GetCacheLevels(out PropertyCacheLevel cacheLevel, out PropertyCacheLevel referenceCacheLevel);
lock (_locko)
lock (_cacheLock)
{
var value = GetInterValue();
hasValue = PropertyType.IsValue(value, PropertyValueLevel.Inter);
@@ -73,7 +88,7 @@ internal sealed class PublishedElementPropertyBase : PublishedPropertyBase
if (!cacheValues.ObjectInitialized)
{
cacheValues.ObjectValue =
PropertyType.ConvertInterToObject(Element, referenceCacheLevel, value, IsPreviewing);
PropertyType.ConvertInterToObject(_element, referenceCacheLevel, value, _isPreviewing);
cacheValues.ObjectInitialized = true;
}
@@ -82,6 +97,7 @@ internal sealed class PublishedElementPropertyBase : PublishedPropertyBase
}
}
/// <inheritdoc />
public override object? GetSourceValue(string? culture = null, string? segment = null) => _sourceValue;
private void GetCacheLevels(out PropertyCacheLevel cacheLevel, out PropertyCacheLevel referenceCacheLevel)
@@ -151,16 +167,17 @@ internal sealed class PublishedElementPropertyBase : PublishedPropertyBase
return _interValue;
}
_interValue = PropertyType.ConvertSourceToInter(Element, _sourceValue, IsPreviewing);
_interValue = PropertyType.ConvertSourceToInter(_element, _sourceValue, _isPreviewing);
_interInitialized = true;
return _interValue;
}
/// <inheritdoc />
public override object? GetValue(string? culture = null, string? segment = null)
{
GetCacheLevels(out PropertyCacheLevel cacheLevel, out PropertyCacheLevel referenceCacheLevel);
lock (_locko)
lock (_cacheLock)
{
CacheValues cacheValues = GetCacheValues(cacheLevel);
if (cacheValues.ObjectInitialized)
@@ -169,12 +186,13 @@ internal sealed class PublishedElementPropertyBase : PublishedPropertyBase
}
cacheValues.ObjectValue =
PropertyType.ConvertInterToObject(Element, referenceCacheLevel, GetInterValue(), IsPreviewing);
PropertyType.ConvertInterToObject(_element, referenceCacheLevel, GetInterValue(), _isPreviewing);
cacheValues.ObjectInitialized = true;
return cacheValues.ObjectValue;
}
}
/// <inheritdoc />
public override object? GetDeliveryApiValue(bool expanding, string? culture = null, string? segment = null)
{
PropertyCacheLevel cacheLevel, referenceCacheLevel;
@@ -187,11 +205,11 @@ internal sealed class PublishedElementPropertyBase : PublishedPropertyBase
GetDeliveryApiCacheLevels(out cacheLevel, out referenceCacheLevel);
}
lock (_locko)
lock (_cacheLock)
{
CacheValues cacheValues = GetCacheValues(cacheLevel);
object? GetDeliveryApiObject() => PropertyType.ConvertInterToDeliveryApiObject(Element, referenceCacheLevel, GetInterValue(), IsPreviewing, expanding);
object? GetDeliveryApiObject() => PropertyType.ConvertInterToDeliveryApiObject(_element, referenceCacheLevel, GetInterValue(), _isPreviewing, expanding);
return expanding
? GetDeliveryApiExpandedObject(cacheValues, GetDeliveryApiObject)
: GetDeliveryApiDefaultObject(cacheValues, GetDeliveryApiObject);
@@ -220,15 +238,15 @@ internal sealed class PublishedElementPropertyBase : PublishedPropertyBase
return cacheValues.DeliveryApiExpandedObjectValue;
}
protected class CacheValues
private class CacheValues
{
public bool ObjectInitialized;
public object? ObjectValue;
public bool XPathInitialized;
public object? XPathValue;
public bool DeliveryApiDefaultObjectInitialized;
public object? DeliveryApiDefaultObjectValue;
public bool DeliveryApiExpandedObjectInitialized;
public object? DeliveryApiExpandedObjectValue;
public bool ObjectInitialized { get; set; }
public object? ObjectValue { get; set; }
public bool XPathInitialized { get; set; }
public object? XPathValue { get; set; }
public bool DeliveryApiDefaultObjectInitialized { get; set; }
public object? DeliveryApiDefaultObjectValue { get; set; }
public bool DeliveryApiExpandedObjectInitialized { get; set; }
public object? DeliveryApiExpandedObjectValue { get; set; }
}
}
+23 -1
View File
@@ -20,7 +20,6 @@ namespace Umbraco.Cms.Core.Routing;
public class DefaultUrlProvider : IUrlProvider
{
private readonly ILocalizationService _localizationService;
private readonly ILocalizedTextService? _localizedTextService;
private readonly ILogger<DefaultUrlProvider> _logger;
private readonly ISiteDomainMapper _siteDomainMapper;
private readonly IUmbracoContextAccessor _umbracoContextAccessor;
@@ -30,6 +29,18 @@ public class DefaultUrlProvider : IUrlProvider
private readonly UriUtility _uriUtility;
private RequestHandlerSettings _requestSettings;
/// <summary>
/// Initializes a new instance of the <see cref="DefaultUrlProvider"/> class.
/// </summary>
/// <param name="requestSettings">The request handler settings.</param>
/// <param name="logger">The logger.</param>
/// <param name="siteDomainMapper">The site domain mapper.</param>
/// <param name="umbracoContextAccessor">The Umbraco context accessor.</param>
/// <param name="uriUtility">The URI utility.</param>
/// <param name="localizationService">The localization service.</param>
/// <param name="navigationQueryService">The document navigation query service.</param>
/// <param name="publishedContentStatusFilteringService">The published content status filtering service.</param>
/// <param name="publishedUrlProvider">The published URL provider.</param>
public DefaultUrlProvider(
IOptionsMonitor<RequestHandlerSettings> requestSettings,
ILogger<DefaultUrlProvider> logger,
@@ -54,6 +65,17 @@ public class DefaultUrlProvider : IUrlProvider
requestSettings.OnChange(x => _requestSettings = x);
}
/// <summary>
/// Initializes a new instance of the <see cref="DefaultUrlProvider"/> class.
/// </summary>
/// <param name="requestSettings">The request handler settings.</param>
/// <param name="logger">The logger.</param>
/// <param name="siteDomainMapper">The site domain mapper.</param>
/// <param name="umbracoContextAccessor">The Umbraco context accessor.</param>
/// <param name="uriUtility">The URI utility.</param>
/// <param name="localizationService">The localization service.</param>
/// <param name="navigationQueryService">The document navigation query service.</param>
/// <param name="publishedContentStatusFilteringService">The published content status filtering service.</param>
[Obsolete("Use the other constructor - Scheduled for removal in V18")]
public DefaultUrlProvider(
IOptionsMonitor<RequestHandlerSettings> requestSettings,
+33 -14
View File
@@ -1,4 +1,4 @@
using System.Globalization;
using System.Globalization;
using Microsoft.Extensions.Options;
using Umbraco.Cms.Core.Configuration.Models;
using Umbraco.Cms.Core.Models;
@@ -7,7 +7,19 @@ using Umbraco.Cms.Core.Web;
namespace Umbraco.Cms.Core.Routing;
// TODO: Kill this, but we need to port all of it's functionality
/// <summary>
/// Represents a legacy published content request.
/// </summary>
/// <remarks>
/// <para>
/// This class is deprecated and scheduled for removal. Its functionality is being
/// migrated to the newer <see cref="IPublishedRequest"/> implementations.
/// </para>
/// <para>
/// TODO: Kill this, but we need to port all of its functionality.
/// </para>
/// </remarks>
[Obsolete("This class is deprecated and will be removed in a future version. Use IPublishedRequest instead. Scheduled for removal in Umbraco 19.")]
public class PublishedRequestOld // : IPublishedRequest
{
private readonly IPublishedRouter _publishedRouter;
@@ -15,13 +27,14 @@ public class PublishedRequestOld // : IPublishedRequest
private CultureInfo? _culture;
private DomainAndUri? _domain;
private bool _is404;
private IPublishedContent? _publishedContent;
private bool _readonly; // after prepared
/// <summary>
/// Initializes a new instance of the <see cref="PublishedRequest" /> class.
/// Initializes a new instance of the <see cref="PublishedRequestOld" /> class.
/// </summary>
/// <param name="publishedRouter">The published router.</param>
/// <param name="umbracoContext">The Umbraco context.</param>
/// <param name="webRoutingSettings">The web routing settings.</param>
/// <param name="uri">The optional URI for this request. If not provided, uses the cleaned Umbraco URL from the context.</param>
public PublishedRequestOld(IPublishedRouter publishedRouter, IUmbracoContext umbracoContext, IOptions<WebRoutingSettings> webRoutingSettings, Uri? uri = null)
{
UmbracoContext = umbracoContext ?? throw new ArgumentNullException(nameof(umbracoContext));
@@ -41,6 +54,9 @@ public class PublishedRequestOld // : IPublishedRequest
/// <remarks>The cleaned up Uri has no virtual directory, no trailing slash, no .aspx extension, etc.</remarks>
public Uri Uri { get; }
/// <summary>
/// Gets or sets a value indicating whether caching should be disabled for this request.
/// </summary>
public bool CacheabilityNoCache { get; set; }
///// <summary>
@@ -102,13 +118,15 @@ public class PublishedRequestOld // : IPublishedRequest
}
}
// utility for ensuring it is ok to set some properties
/// <summary>
/// Ensures the request is in a writeable state.
/// </summary>
/// <remarks>
/// This method is a no-op. The readonly check was removed because the backing field
/// was never set to true, making the check unnecessary. Retained for API compatibility.
/// </remarks>
public void EnsureWriteable()
{
if (_readonly)
{
throw new InvalidOperationException("Cannot modify a PublishedRequest once it is read-only.");
}
}
// #region Events
@@ -226,18 +244,19 @@ public class PublishedRequestOld // : IPublishedRequest
/// <summary>
/// Gets value indicating whether the current published content is the initial one.
/// </summary>
public bool IsInitialPublishedContent =>
InitialPublishedContent != null && InitialPublishedContent == _publishedContent;
/// <remarks>Always returns false as _publishedContent field was removed (never assigned).</remarks>
public bool IsInitialPublishedContent => false;
/// <summary>
/// Indicates that the current PublishedContent is the initial one.
/// </summary>
/// <remarks>Note: _publishedContent field was removed (never assigned), so this sets InitialPublishedContent to null.</remarks>
public void SetIsInitialPublishedContent()
{
EnsureWriteable();
// note: it can very well be null if the initial content was not found
InitialPublishedContent = _publishedContent;
InitialPublishedContent = null;
IsInternalRedirectPublishedContent = false;
}
@@ -25,7 +25,9 @@ public class ContentPermissionResource : IPermissionResource
/// <param name="contentKey">The key of the content or null if root.</param>
/// <param name="cultures">The cultures to validate</param>
/// <returns>An instance of <see cref="ContentPermissionResource" />.</returns>
public static ContentPermissionResource WithKeys(string permissionToCheck, Guid? contentKey,
public static ContentPermissionResource WithKeys(
string permissionToCheck,
Guid? contentKey,
IEnumerable<string> cultures) =>
contentKey is null
? Root(permissionToCheck, cultures)
@@ -61,7 +63,9 @@ public class ContentPermissionResource : IPermissionResource
/// <param name="contentKey">The key of the content.</param>
/// <param name="cultures">The required culture access</param>
/// <returns>An instance of <see cref="ContentPermissionResource" />.</returns>
public static ContentPermissionResource WithKeys(string permissionToCheck, Guid contentKey,
public static ContentPermissionResource WithKeys(
string permissionToCheck,
Guid contentKey,
IEnumerable<string> cultures) => WithKeys(permissionToCheck, contentKey.Yield(), cultures);
/// <summary>
@@ -80,7 +84,9 @@ public class ContentPermissionResource : IPermissionResource
/// <param name="contentKeys">The keys of the contents.</param>
/// <param name="cultures">The required culture access</param>
/// <returns>An instance of <see cref="ContentPermissionResource" />.</returns>
public static ContentPermissionResource WithKeys(string permissionToCheck, IEnumerable<Guid> contentKeys,
public static ContentPermissionResource WithKeys(
string permissionToCheck,
IEnumerable<Guid> contentKeys,
IEnumerable<string> cultures) =>
new(
contentKeys,
@@ -114,7 +120,12 @@ public class ContentPermissionResource : IPermissionResource
/// <param name="cultures">The cultures to validate</param>
/// <returns>An instance of <see cref="ContentPermissionResource" />.</returns>
public static ContentPermissionResource Root(string permissionToCheck, IEnumerable<string> cultures) =>
new(Enumerable.Empty<Guid>(), new HashSet<string> { permissionToCheck }, true, false, null,
new(
Enumerable.Empty<Guid>(),
new HashSet<string> { permissionToCheck },
true,
false,
null,
new HashSet<string>(cultures));
/// <summary>
@@ -176,7 +187,9 @@ public class ContentPermissionResource : IPermissionResource
/// <param name="parentKeyForBranch">The parent key of the branch.</param>
/// <param name="culturesToCheck">The required cultures</param>
/// <returns>An instance of <see cref="ContentPermissionResource" />.</returns>
public static ContentPermissionResource Branch(string permissionToCheck, Guid parentKeyForBranch,
public static ContentPermissionResource Branch(
string permissionToCheck,
Guid parentKeyForBranch,
IEnumerable<string> culturesToCheck) =>
new(
Enumerable.Empty<Guid>(),
@@ -189,7 +202,8 @@ public class ContentPermissionResource : IPermissionResource
private ContentPermissionResource(
IEnumerable<Guid> contentKeys,
ISet<string> permissionsToCheck,
bool checkRoot, bool checkRecycleBin,
bool checkRoot,
bool checkRecycleBin,
Guid? parentKeyForBranch,
ISet<string>? culturesToCheck)
{
@@ -12,17 +12,25 @@ namespace Umbraco.Cms.Core.Security;
/// </remarks>
public class LegacyPasswordSecurity
{
/// <summary>
/// Generates a cryptographically secure random salt.
/// </summary>
/// <returns>A base64-encoded salt string.</returns>
public static string GenerateSalt()
{
var numArray = new byte[16];
using (var rng = new RNGCryptoServiceProvider())
{
rng.GetBytes(numArray);
return Convert.ToBase64String(numArray);
}
RandomNumberGenerator.Fill(numArray);
return Convert.ToBase64String(numArray);
}
// Used for tests
/// <summary>
/// Formats a password for storage by combining salt and hashed password.
/// </summary>
/// <param name="algorithmType">The hashing algorithm type.</param>
/// <param name="hashedPassword">The hashed password.</param>
/// <param name="salt">The salt used for hashing.</param>
/// <returns>The formatted password string for storage.</returns>
/// <remarks>Used for tests.</remarks>
internal string FormatPasswordForStorage(string algorithmType, string hashedPassword, string salt)
{
if (!SupportHashAlgorithm(algorithmType))
@@ -34,12 +42,12 @@ public class LegacyPasswordSecurity
}
/// <summary>
/// Verifies if the password matches the expected hash+salt of the stored password string
/// Verifies if the password matches the expected hash+salt of the stored password string.
/// </summary>
/// <param name="algorithm">The hashing algorithm for the stored password.</param>
/// <param name="password">The password.</param>
/// <param name="password">The password to verify.</param>
/// <param name="dbPassword">The value of the password stored in a data store.</param>
/// <returns></returns>
/// <returns><c>true</c> if the password matches; otherwise, <c>false</c>.</returns>
public bool VerifyPassword(string algorithm, string password, string dbPassword)
{
if (string.IsNullOrWhiteSpace(dbPassword))
@@ -66,8 +74,11 @@ public class LegacyPasswordSecurity
}
/// <summary>
/// Verify a legacy hashed password (HMACSHA1)
/// Verifies a legacy hashed password that was created using HMACSHA1.
/// </summary>
/// <param name="password">The password to verify.</param>
/// <param name="dbPassword">The stored hashed password from the database.</param>
/// <returns><c>true</c> if the password matches; otherwise, <c>false</c>.</returns>
public bool VerifyLegacyHashedPassword(string password, string dbPassword)
{
using var hashAlgorithm = new HMACSHA1
@@ -82,8 +93,12 @@ public class LegacyPasswordSecurity
}
/// <summary>
/// Create a new password hash and a new salt
/// Creates a new password hash with a newly generated salt.
/// </summary>
/// <param name="algorithm">The hashing algorithm to use.</param>
/// <param name="newPassword">The password to hash.</param>
/// <param name="salt">Returns the generated salt.</param>
/// <returns>The hashed password.</returns>
internal string HashNewPassword(string algorithm, string newPassword, out string salt)
{
salt = GenerateSalt();
@@ -91,12 +106,12 @@ public class LegacyPasswordSecurity
}
/// <summary>
/// Parses out the hashed password and the salt from the stored password string value
/// Parses out the hashed password and the salt from the stored password string value.
/// </summary>
/// <param name="algorithm">The hashing algorithm for the stored password.</param>
/// <param name="storedString"></param>
/// <param name="salt">returns the salt</param>
/// <returns></returns>
/// <param name="storedString">The stored password string containing salt and hash.</param>
/// <param name="salt">Returns the extracted salt.</param>
/// <returns>The hashed password portion of the stored string.</returns>
public string ParseStoredHashPassword(string algorithm, string storedString, out string salt)
{
if (string.IsNullOrWhiteSpace(storedString))
@@ -114,6 +129,11 @@ public class LegacyPasswordSecurity
return storedString.Substring(saltLen.Length);
}
/// <summary>
/// Determines whether the specified hash algorithm is supported.
/// </summary>
/// <param name="algorithm">The algorithm name to check.</param>
/// <returns><c>true</c> if the algorithm is supported; otherwise, <c>false</c>.</returns>
public bool SupportHashAlgorithm(string algorithm)
{
// This is for the v6-v8 hashing algorithm
@@ -132,12 +152,12 @@ public class LegacyPasswordSecurity
}
/// <summary>
/// Hashes a password with a given salt
/// Hashes a password with a given salt.
/// </summary>
/// <param name="algorithmType">The hashing algorithm for the password.</param>
/// <param name="pass"></param>
/// <param name="salt"></param>
/// <returns></returns>
/// <param name="pass">The password to hash.</param>
/// <param name="salt">The salt to use for hashing.</param>
/// <returns>The hashed password as a base64-encoded string.</returns>
private string HashPassword(string algorithmType, string pass, string salt)
{
if (!SupportHashAlgorithm(algorithmType))
@@ -195,10 +215,11 @@ public class LegacyPasswordSecurity
}
/// <summary>
/// Return the hash algorithm to use based on the provided <paramref name="algorithm"/>.
/// Returns the hash algorithm instance based on the provided algorithm name.
/// </summary>
/// <param name="algorithm">The hashing algorithm name.</param>
/// <returns></returns>
/// <param name="algorithm">The hashing algorithm name (e.g., "HMACSHA256", "SHA1", "MD5").</param>
/// <returns>A <see cref="HashAlgorithm"/> instance for the specified algorithm.</returns>
/// <exception cref="InvalidOperationException">Thrown when the algorithm is not specified or not recognized.</exception>
private HashAlgorithm GetHashAlgorithm(string algorithm)
{
if (algorithm.IsNullOrWhiteSpace())
@@ -206,12 +227,16 @@ public class LegacyPasswordSecurity
throw new InvalidOperationException("No hash algorithm type specified");
}
var alg = HashAlgorithm.Create(algorithm);
if (alg == null)
return algorithm.ToUpperInvariant() switch
{
throw new InvalidOperationException($"The hash algorithm specified {algorithm} cannot be resolved");
}
return alg;
"HMACSHA256" => new HMACSHA256(),
"HMACSHA1" => new HMACSHA1(),
"SHA1" => SHA1.Create(),
"SHA256" => SHA256.Create(),
"SHA384" => SHA384.Create(),
"SHA512" => SHA512.Create(),
"MD5" => MD5.Create(),
_ => throw new InvalidOperationException($"The hash algorithm specified {algorithm} cannot be resolved")
};
}
}
@@ -98,10 +98,7 @@ public class PasswordGenerator
var data = new byte[length];
var chArray = new char[length];
var num1 = 0;
using (var rng = new RNGCryptoServiceProvider())
{
rng.GetBytes(data);
}
RandomNumberGenerator.Fill(data);
for (var index = 0; index < length; ++index)
{
@@ -292,7 +292,9 @@ internal sealed class ContentEditingService
public async Task<Attempt<IContent?, ContentEditingOperationStatus>> CopyAsync(Guid key, Guid? parentKey, bool relateToOriginal, bool includeDescendants, Guid userKey)
=> await HandleCopyAsync(key, parentKey, relateToOriginal, includeDescendants, userKey);
public async Task<ContentEditingOperationStatus> SortAsync(Guid? parentKey, IEnumerable<SortingModel> sortingModels,
public async Task<ContentEditingOperationStatus> SortAsync(
Guid? parentKey,
IEnumerable<SortingModel> sortingModels,
Guid userKey)
=> await HandleSortAsync(parentKey, sortingModels, userKey);

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