Compare commits

...
Author SHA1 Message Date
Andreas ZerbstandGitHub 9ab0abca99 E2E: Stop running version 14 on nightly test pipeline (#20558)
Stops 14 from running on our nightly test pipeline
2025-10-20 12:05:42 +02:00
Andy Butland 009a999e22 Merge branch 'release/14.3.4' into v14/dev 2025-04-08 07:15:06 +02:00
Andy ButlandandGitHub d3c1443b14 Merge commit from fork
* Prevent path traveral vulnerability with upload of temporary files.

* Used BadRequest instead of NotFound for invalid file name response.
2025-04-08 05:03:40 +02:00
Andy Butland 8a9db11422 Merge branch 'release/14.3.3' into v14/dev
# Conflicts:
#	Directory.Packages.props
#	version.json
2025-03-11 09:28:52 +01:00
Andy ButlandandGitHub d9fb6df16e Merge commit from fork
* Bumped version to 15.2.1.

# Conflicts:
#	version.json

* Tighten management API endpoint access rules.
2025-03-11 05:11:35 +01:00
Andy Butland 14ed3348bf bumped imagesharp to prevent CVE-2025-27598 (#18602)
# Conflicts:
#	Directory.Packages.props
2025-03-09 08:54:03 +01:00
Sven Geusens 9e7a36865a Merge branch 'v13/dev' into v14/dev
Revert #18249 as it is reimplemented for v15
Revert #18320 as the new architecture explictly throws an error

# Conflicts:
#	build/azure-pipelines.yml
#	src/Umbraco.Core/EmbeddedResources/Lang/en.xml
#	src/Umbraco.Core/EmbeddedResources/Lang/en_us.xml
#	src/Umbraco.Core/Models/ContentEditing/ContentSaveAction.cs
#	src/Umbraco.Core/Services/ContentService.cs
#	src/Umbraco.Core/Services/IContentService.cs
#	src/Umbraco.Core/Services/MemberService.cs
#	src/Umbraco.Infrastructure/PropertyEditors/RichTextEditorPastedImages.cs
#	src/Umbraco.Infrastructure/Security/BackOfficeUserStore.cs
#	src/Umbraco.Infrastructure/Security/MemberUserStore.cs
#	src/Umbraco.Web.BackOffice/Controllers/ContentController.cs
#	src/Umbraco.Web.BackOffice/Controllers/EntityController.cs
#	src/Umbraco.Web.BackOffice/Controllers/MediaController.cs
#	src/Umbraco.Web.BackOffice/Controllers/MemberController.cs
#	src/Umbraco.Web.BackOffice/Controllers/PreviewController.cs
#	src/Umbraco.Web.BackOffice/Controllers/UsersController.cs
#	src/Umbraco.Web.BackOffice/DependencyInjection/UmbracoBuilder.BackOfficeAuth.cs
#	src/Umbraco.Web.BackOffice/Filters/ContentSaveValidationAttribute.cs
#	src/Umbraco.Web.BackOffice/Filters/MemberSaveModelValidator.cs
#	src/Umbraco.Web.BackOffice/Filters/MemberSaveValidationAttribute.cs
#	src/Umbraco.Web.BackOffice/Trees/ContentTreeController.cs
#	src/Umbraco.Web.Common/RuntimeMinification/SmidgeOptionsSetup.cs
#	src/Umbraco.Web.Common/RuntimeMinification/SmidgeRuntimeMinifier.cs
#	src/Umbraco.Web.Common/Views/UmbracoViewPage.cs
#	src/Umbraco.Web.UI.Client/src/common/directives/components/buttons/umbbuttongroup.directive.js
#	src/Umbraco.Web.UI.Client/src/common/directives/components/content/edit.controller.js
#	src/Umbraco.Web.UI.Client/src/common/directives/components/content/umbtabbedcontent.directive.js
#	src/Umbraco.Web.UI.Client/src/common/directives/components/property/umbproperty.directive.js
#	src/Umbraco.Web.UI.Client/src/common/resources/content.resource.js
#	src/Umbraco.Web.UI.Client/src/common/resources/publicaccess.resource.js
#	src/Umbraco.Web.UI.Client/src/common/resources/users.resource.js
#	src/Umbraco.Web.UI.Client/src/common/services/assets.service.js
#	src/Umbraco.Web.UI.Client/src/views/common/drawers/help/help.controller.js
#	src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/linkpicker/linkpicker.controller.js
#	src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/mediaentryeditor/mediaentryeditor.controller.js
#	src/Umbraco.Web.UI.Client/src/views/components/content/umb-tabbed-content.html
#	src/Umbraco.Web.UI.Client/src/views/components/property/umb-property.html
#	src/Umbraco.Web.UI.Client/src/views/content/content.create.controller.js
#	src/Umbraco.Web.UI.Client/src/views/content/overlays/publishdescendants.controller.js
#	src/Umbraco.Web.UI.Client/src/views/content/overlays/publishdescendants.html
#	src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/listview.controller.js
#	src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/rte.component.js
#	src/Umbraco.Web.UI.Client/src/views/users/views/user/details.html
#	src/Umbraco.Web.UI.Client/src/views/webhooks/edit.controller.js
#	src/Umbraco.Web.UI.Client/src/views/webhooks/edit.html
#	src/Umbraco.Web.UI.Client/test/unit/app/content/create-content-controller.spec.js
#	src/Umbraco.Web.UI.Client~HEAD
#	src/Umbraco.Web.UI.Login/src/auth.element.ts
#	tests/Umbraco.TestData/UmbracoTestDataController.cs
#	tests/Umbraco.Tests.Integration/Umbraco.Core/Services/ContentServiceTests.cs
#	tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentEventsTests.cs
#	tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentServicePublishBranchTests.cs
#	tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentServiceTagsTests.cs
#	tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Security/MemberUserStoreTests.cs
#	tests/Umbraco.Tests.UnitTests/Umbraco.Web.BackOffice/Controllers/MemberControllerUnitTests.cs
#	version.json
2025-02-17 19:25:45 +01:00
Andy ButlandandGitHub db1d999721 Avoid exception when attempting to find member by Id when Id is not an expected Guid or integer, as can be the case with external member providers. (#18320) 2025-02-17 12:47:52 +01:00
Andy ButlandandGitHub 18047a7cfb Only filter post retrieval of entities for start nodes if working with entities that support start nodes. (#18287) 2025-02-17 12:45:51 +01:00
Andy ButlandandGitHub def7ebd48c Html encodes the user's name in the invite email. (#18343) 2025-02-17 12:25:12 +01:00
8c2b1ebdc5 V13: Introduce publishNotifications method on IMembershipMemberService (#18207)
* Introduce publishNotifications method on IMembershipMemberService.cs

* Fix test

* Add PublishNotificationSaveOptions

* Fix up according to comments

* Use numeric values for flag enum

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

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

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

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

---------

Co-authored-by: Andy Butland <abutland73@gmail.com>
2025-02-12 13:30:41 +01:00
Andy ButlandandGitHub 9227517a50 Split force for publish descendants into separate options for publish unpublish and re-publish unedited (13) (#18249)
* Split force for publish descendents into separate options for publish unpublish and re-publish unedited.

* Added integration task verifying updated behaviour.

* Variant integration test.

* Update test data controller.

* Remove usued function parameters.

* Refactor to enum.

* Fixed flags enum.

* Variable name refactor.

* Applied changes from code review.

* Refactored method name.

* Aligned js boolean checks.
2025-02-12 11:30:27 +00:00
Andy Butland 5322d0f7b5 Bumped version to 13.8.0-rc. 2025-02-12 08:13:58 +01:00
Andy ButlandandGitHub a282cc5691 Backport use of thread delay over sleep and handle dispose in FileSystemMainDomLock (#18151)
* Backport use of thread delay over sleep and handle dispose in FileSystemMainDomLock (from PRs #18119 and #18147)

* Applied suggestion from code review.
2025-02-12 08:06:50 +01:00
Andy ButlandandGitHub 048f8bcdf9 Fixed userResource request to get all users. (#18105) 2025-02-11 17:06:18 +01:00
Andy ButlandandGitHub 7bcbc748d4 URL encodes member user names when passing information for public access setting such that those with user names as emails containing a plus will be included in the rule. (#18142) 2025-02-11 13:36:19 +01:00
Andy ButlandandGitHub f30e6cfe7f Prevents folder selection in media picker when used from the multi URL picker. (#18288) 2025-02-11 09:18:01 +01:00
Andy ButlandandGitHub 026e80e026 Avoid an exception on sign out when the principal is populated from an incomplete external login (#18078)
* Avoid an exception on signout when the principal is populated from an incomplete external login.

* Tidied up comment.
2025-02-10 12:51:35 +01:00
Andy ButlandandGitHub a3b77cff63 Add validation to prevent update of a user or member to an invalid username (13) (#18261)
* Add validation to prevent update of a user or member to an invalid username.

* Avoid password manager updates of user name field on user details screen.
2025-02-10 10:40:53 +01:00
Niels LyngsøandGitHub 4ca68d6995 Add client-side validation for RTE (#18257) 2025-02-07 11:43:04 +00:00
Sven GeusensandGitHub 280cb7f2b1 Fix issues in newly added buttongroup localization (#18254)
* Fix #18253 nullref exception

* Fix #18239 by listening to broader scope changes
2025-02-07 07:00:34 +01:00
095a73132c Review: Allow Duplicate Email for Members (#16202)
* init

* Aligned default values on security settings.

* Added validator for security settings.

* Provide default implementation for get members by email.

* Refactored constructor of MemberController.

* Validate on unique member email only when configured to do so.

* Further code tidy and use of DI in constructor.

* Used new constructor in tests.

* Add unit test for modified behaviour.

* Removed validator for security settings (it's not necessary, I got confused with users and members).

* Spelling.

---------

Co-authored-by: Andy Butland <abutland73@gmail.com>
2025-02-05 12:38:40 +01:00
Andy Butland cfb0fc23ac Merge branch 'v13/dev' into v13/contrib 2025-02-04 12:35:49 +01:00
6620aca9fe Cache null dictionary values by key (#15576)
* Add CacheNullValues option to RepositoryCachePolicy

* Cache null values in DictionaryByKeyRepository

* Fixed issue with nullable reference.

* Updated logic for caching of null values.

* Update src/Umbraco.Infrastructure/Cache/DefaultRepositoryCachePolicy.cs

Co-authored-by: Sven Geusens <geusens@gmail.com>

* Made the NullValueRepresentation overwritable in a generic manner

* Improve generic NullValueCachePolicyResolver

* Revert Commits and clarify logic with comment

This reverts commit 8befb43792 "Improve generic NullValueCachePolicyResolver"
Also reverts 8adf0a2 - Made the NullValueRepresentation overwritable in a generic manner
And 8adf0a2 - Made the NullValueRepresentation overwritable in a generic manner

* Update src/Umbraco.Infrastructure/Cache/DefaultRepositoryCachePolicy.cs

---------

Co-authored-by: Andy Butland <abutland73@gmail.com>
Co-authored-by: Sven Geusens <geusens@gmail.com>
Co-authored-by: Sven Geusens <sge@umbraco.dk>
2025-02-04 12:29:21 +01:00
2422a02b23 Set Smidge cachebuster type (#18198)
* Set Smidge cachebuster type

* Amend exception and fix typo

* Minor tweak to comment and exception message.

---------

Co-authored-by: Simon Hartfield <simon.hartfield@googlemail.com>
Co-authored-by: Andy Butland <abutland73@gmail.com>
2025-02-04 11:00:44 +00:00
b4a9dc0770 V13: Fix members while using basic auth. (#18206)
* Flow additional identities to new principal

* Add extension to more easily get member identity

* Ensure the member is used instead of the backoffice user in `MemberManager`

* Update snippet

* Fix the comment that I broke

* Update src/Umbraco.Web.Common/Extensions/MemberClaimsPrincipalExtensions.cs

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

---------

Co-authored-by: Andy Butland <abutland73@gmail.com>
2025-02-03 19:48:08 +01:00
Andy ButlandandGitHub e7411244fd Show notifications menu only to users with permission for the feature. (#18184) 2025-02-03 13:24:58 +01:00
Kenn JacobsenandGitHub 9f357173c3 Enforce user start nodes for media uploads through the RTE (#18204) 2025-02-03 12:50:23 +01:00
Andy Butland 7dc6e3ed5b Merge branch 'v13/dev' into v13/contrib 2025-01-31 16:16:22 +01:00
Gareth WrightandGitHub 5dfff212d0 Update auth.element.ts (#18192)
auth_username => general_username
2025-01-31 14:59:25 +01:00
17615f966b Fix out of memory of 2gb+ (max 4gb) error introduced by #14657 SVG xss security fix (#17421)
Co-authored-by: Andy Butland <abutland73@gmail.com>
2025-01-31 13:46:56 +00:00
Kenn JacobsenandGitHub 7a2d6b6c63 More robust resolving of Delivery API redirects (#18160) 2025-01-31 14:26:07 +01:00
Sven Geusens 343867f6ee Merge branch 'release/13.6' into v13/dev and version bump
# Conflicts:
#	src/Umbraco.Web.BackOffice/Controllers/PreviewController.cs
#	version.json
2025-01-30 16:27:49 +01:00
Bjarke BergandSven Geusens b9837ac77c Optimize Azure pipeline (#17674)
* Only fetch single commit

* Hopefully fixes Nerdbank.GitVersioning.GitException: Shallow clone lacks the objects required to calculate version height. Use full clones or clones with a history at least as deep as the last version height resetting change.

* Do not checkout again

* More test pipeline

* Another attempt

* yet another attempt

* more attempts

* Revert "more attempts"

This reverts commit 5694d97ba6.

* Test without building backoffice and login explicitly

* Fix mem leak in integration tests

* Fixes sqlserver lock test
# Conflicts:
#	build/azure-pipelines.yml
2025-01-30 14:10:05 +01:00
Sven Geusens bb73ec6c68 Set release version 2025-01-30 09:52:50 +01:00
Andy ButlandandGitHub ee2d7bbb1b Excluded tags from trashed content (#18164) 2025-01-29 21:10:11 +01:00
Andy ButlandandGitHub 5d48bc7371 Provides an option to remove the inessential version number from the generated models (#18081)
* Provides an option to remove the inessential version number from the generated models.

* Clarified comment.
2025-01-29 13:33:49 +01:00
Kenn JacobsenandGitHub 59a4649528 Do not allow editing read-only properties by clicking their labels (#18152)
* Do not allow editing read-only properties by clicking their labels

* Simplify the fix :)

* Fix linting issue
2025-01-29 10:29:58 +01:00
Andy ButlandandGitHub f54b6033ab Added Resharper test assemblies to exclude list on TypeFinder. (#18145) 2025-01-29 10:27:17 +01:00
Jacob OvergaardandGitHub bf340cd7d4 fix: remove unused parameters and documentation (#18095)
this fixes an issue where unused parameters were published as supported, but they were in fact never supported.
2025-01-28 10:53:00 +00:00
Andy Butland 313417cb91 Tidied up XML header comment in ITagQuery. 2025-01-28 10:52:21 +01:00
7850078623 Redirect to the published URL when exiting preview (#18114)
Co-authored-by: Andy Butland <abutland73@gmail.com>
2025-01-24 18:46:28 +00:00
44bf3b77b3 Make it possible to reset media picker crops (#18110)
Co-authored-by: Andy Butland <abutland73@gmail.com>
2025-01-24 13:41:30 +00:00
Kenn JacobsenandGitHub 8485458896 Add clientside validation to webhook events (#18089) 2025-01-23 17:01:55 +01:00
Nikolaj Geisle 3d253f5f06 bump version 2025-01-23 11:15:26 +01:00
Nikolaj Geisle 64287d7e1a Merge branch 'release/13.5.3' into release/13.6
# Conflicts:
#	version.json
2025-01-23 11:14:47 +01:00
Andy ButlandandGitHub edc78a5a4c Handles migration case where an expected constraint is renamed but the constraint does not exist. (#18063) 2025-01-22 12:26:06 +01:00
Andy Butland 8056a59a10 Fix schema defined in JSON schema tool definition after merge from 13. 2025-01-21 15:52:31 +01:00
1a18d6c035 Replaced deprecated navigator.platform with navigator.userAgent for platform detection. (#17373)
Co-authored-by: Niels Lyngsø <nsl@umbraco.dk>
2025-01-21 13:56:40 +01:00
Andy Butland 3e877182b5 Merge branch 'v13/contrib' into v13/dev 2025-01-21 09:47:20 +01:00
Martin VennevoldandGitHub 0e4f883bc1 Fix create child issue in list view with infinite editor (#13355). (#17637) 2025-01-21 09:44:22 +01:00
Andy Butland 265cc62ab8 Merge branch 'release/13.6' into v13/dev 2025-01-21 09:21:36 +01:00
Andy Butland 800c16970e Merge branch 'release/13.6' of https://github.com/umbraco/Umbraco-CMS into release/13.6 2025-01-21 09:20:11 +01:00
Andy Butland 6bd11bf233 Fixes failing front-end unit test to align with new behaviour from PR #17818 (
Allow skipSelect blueprints only when one blueprint exists).
2025-01-21 09:19:31 +01:00
Kenn Jacobsen a2fd82a3f3 Add NoopCurrentMemberClaimsProvider so Umbraco can boot without the Delivery API enabled (#18049) 2025-01-21 09:13:31 +01:00
Kenn JacobsenandGitHub 7567990da1 Add NoopCurrentMemberClaimsProvider so Umbraco can boot without the Delivery API enabled (#18049) 2025-01-21 09:10:43 +01:00
Zeegaan c7d157bbfd build on ubuntu 2025-01-21 09:02:00 +01:00
Jacob Overgaard abc312c9b4 update backoffice submodule to 14.3.2 2025-01-21 08:44:10 +01:00
Nikolaj Geisle 2161edb871 Bump version 2025-01-21 07:41:06 +01:00
Nikolaj Geisle e77e9c5691 Format sql statement (#17354)
(cherry picked from commit aa9f194d76)
2025-01-20 18:27:54 +01:00
Nikolaj Geisle e934a943b5 build on windows 2025-01-20 17:37:04 +01:00
Ronald BarendseandNikolaj Geisle 65bb2801b0 Merge commit from fork
* Add TimedScope

* Use TimedScope in login endpoint

* Use seperate default duration and only calculate average of actual successful responses

* Only return detailed error responses if credentials are valid

* Cancel timed scope when credentials are valid

* Add UserDefaultFailedLoginDuration and UserMinimumFailedLoginDuration settings
2025-01-20 16:44:28 +01:00
Andy ButlandandGitHub a0a4af6a0c Merge commit from fork 2025-01-20 14:54:14 +01:00
Jacob Overgaard e8d6cded2b update backoffice submodule to 14.3.2 2025-01-20 14:42:17 +01:00
Bjarke BergandJacob Overgaard c65204a146 update backoffice submodule to 14.3.2 2025-01-20 14:34:03 +01:00
Andy ButlandandGitHub 95eb58587b Merge commit from fork 2025-01-20 14:14:28 +01:00
Andy ButlandandGitHub 86f3033334 Merge commit from fork 2025-01-20 14:14:28 +01:00
25628a8b76 Lucene Package Update to Address CVE-2024-43383 (#17942)
* Update Lucene Package to 4.8.0-beta00017

* Add Package Reference

---------

Co-authored-by: Sebastiaan Janssen <sebastiaan@umbraco.com>
2025-01-20 11:45:35 +01:00
Callum WhyteandAndy Butland 7552e315fb Allow skipSelect blueprints only when one blueprint exists (#17818) 2025-01-20 11:45:26 +01:00
Callum WhyteandGitHub d15b5b221e Allow skipSelect blueprints only when one blueprint exists (#17818) 2025-01-20 10:27:54 +01:00
Andy Butland 0166727eee Bumped version to 14.3.2. 2025-01-07 10:13:02 +01:00
52 changed files with 887 additions and 154 deletions
+1 -1
View File
@@ -73,7 +73,7 @@
<PackageVersion Include="Serilog.Sinks.Async" Version="1.5.0" />
<PackageVersion Include="Serilog.Sinks.File" Version="5.0.0" />
<PackageVersion Include="Serilog.Sinks.Map" Version="1.0.2" />
<PackageVersion Include="SixLabors.ImageSharp" Version="3.1.6" />
<PackageVersion Include="SixLabors.ImageSharp" Version="3.1.7" />
<PackageVersion Include="SixLabors.ImageSharp.Web" Version="3.1.3" />
<PackageVersion Include="Swashbuckle.AspNetCore" Version="6.9.0" />
</ItemGroup>
+6 -7
View File
@@ -3,13 +3,12 @@ name: Nightly_E2E_Test_$(TeamProject)_$(Build.DefinitionName)_$(SourceBranchName
pr: none
trigger: none
schedules:
- cron: '0 0 * * *'
displayName: Daily midnight build
branches:
include:
- v14/dev
- v15/dev
# schedules:
# - cron: '0 0 * * *'
# displayName: Daily midnight build
# branches:
# include:
# - v14/dev
variables:
nodeVersion: 20
@@ -42,31 +42,42 @@ internal sealed class RequestRedirectService : RoutingServiceBase, IRequestRedir
{
requestedPath = requestedPath.EnsureStartsWith("/");
IPublishedContent? startItem = GetStartItem();
// must append the root content url segment if it is not hidden by config, because
// the URL tracking is based on the actual URL, including the root content url segment
if (_globalSettings.HideTopLevelNodeFromPath == false)
if (_globalSettings.HideTopLevelNodeFromPath == false && startItem?.UrlSegment != null)
{
IPublishedContent? startItem = GetStartItem();
if (startItem?.UrlSegment != null)
{
requestedPath = $"{startItem.UrlSegment.EnsureStartsWith("/")}{requestedPath}";
}
requestedPath = $"{startItem.UrlSegment.EnsureStartsWith("/")}{requestedPath}";
}
var culture = _requestCultureService.GetRequestedCulture();
// append the configured domain content ID to the path if we have a domain bound request,
// because URL tracking registers the tracked url like "{domain content ID}/{content path}"
Uri contentRoute = GetDefaultRequestUri(requestedPath);
DomainAndUri? domainAndUri = GetDomainAndUriForRoute(contentRoute);
if (domainAndUri != null)
// important: redirect URLs are always tracked without trailing slashes
requestedPath = requestedPath.TrimEnd("/");
IRedirectUrl? redirectUrl = _redirectUrlService.GetMostRecentRedirectUrl(requestedPath, culture);
// if a redirect URL was not found, try by appending the start item ID because URL tracking might have tracked
// a redirect with "{root content ID}/{content path}"
if (redirectUrl is null && startItem is not null)
{
requestedPath = GetContentRoute(domainAndUri, contentRoute);
culture ??= domainAndUri.Culture;
redirectUrl = _redirectUrlService.GetMostRecentRedirectUrl($"{startItem.Id}{requestedPath}", culture);
}
// still no redirect URL found - try looking for a configured domain if we have a domain bound request,
// because URL tracking might have tracked a redirect with "{domain content ID}/{content path}"
if (redirectUrl is null)
{
Uri contentRoute = GetDefaultRequestUri(requestedPath);
DomainAndUri? domainAndUri = GetDomainAndUriForRoute(contentRoute);
if (domainAndUri is not null)
{
requestedPath = GetContentRoute(domainAndUri, contentRoute);
culture ??= domainAndUri.Culture;
redirectUrl = _redirectUrlService.GetMostRecentRedirectUrl(requestedPath, culture);
}
}
// important: redirect URLs are always tracked without trailing slashes
IRedirectUrl? redirectUrl = _redirectUrlService.GetMostRecentRedirectUrl(requestedPath.TrimEnd("/"), culture);
IPublishedContent? content = redirectUrl != null
? _apiPublishedContentCache.GetById(redirectUrl.ContentKey)
: null;
@@ -1,4 +1,5 @@
using Asp.Versioning;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Api.Management.ViewModels.DataType;
@@ -7,10 +8,12 @@ using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Security;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Services.OperationStatus;
using Umbraco.Cms.Web.Common.Authorization;
namespace Umbraco.Cms.Api.Management.Controllers.DataType;
[ApiVersion("1.0")]
[Authorize(Policy = AuthorizationPolicies.TreeAccessDataTypes)]
public class CopyDataTypeController : DataTypeControllerBase
{
private readonly IDataTypeService _dataTypeService;
@@ -1,4 +1,5 @@
using Asp.Versioning;
using Asp.Versioning;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Api.Management.Factories;
@@ -8,10 +9,12 @@ using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Security;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Services.OperationStatus;
using Umbraco.Cms.Web.Common.Authorization;
namespace Umbraco.Cms.Api.Management.Controllers.DataType;
[ApiVersion("1.0")]
[Authorize(Policy = AuthorizationPolicies.TreeAccessDataTypes)]
public class CreateDataTypeController : DataTypeControllerBase
{
private readonly IDataTypeService _dataTypeService;
@@ -1,4 +1,5 @@
using Asp.Versioning;
using Asp.Versioning;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Core;
@@ -6,10 +7,12 @@ using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Security;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Services.OperationStatus;
using Umbraco.Cms.Web.Common.Authorization;
namespace Umbraco.Cms.Api.Management.Controllers.DataType;
[ApiVersion("1.0")]
[Authorize(Policy = AuthorizationPolicies.TreeAccessDataTypes)]
public class DeleteDataTypeController : DataTypeControllerBase
{
private readonly IDataTypeService _dataTypeService;
@@ -1,4 +1,5 @@
using Asp.Versioning;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Api.Management.ViewModels.DataType;
@@ -7,10 +8,12 @@ using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Security;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Services.OperationStatus;
using Umbraco.Cms.Web.Common.Authorization;
namespace Umbraco.Cms.Api.Management.Controllers.DataType;
[ApiVersion("1.0")]
[Authorize(Policy = AuthorizationPolicies.TreeAccessDataTypes)]
public class MoveDataTypeController : DataTypeControllerBase
{
private readonly IDataTypeService _dataTypeService;
@@ -1,4 +1,5 @@
using Asp.Versioning;
using Asp.Versioning;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Api.Management.Factories;
@@ -8,10 +9,12 @@ using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Security;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Services.OperationStatus;
using Umbraco.Cms.Web.Common.Authorization;
namespace Umbraco.Cms.Api.Management.Controllers.DataType;
[ApiVersion("1.0")]
[Authorize(Policy = AuthorizationPolicies.TreeAccessDataTypes)]
public class UpdateDataTypeController : DataTypeControllerBase
{
private readonly IDataTypeService _dataTypeService;
@@ -1,14 +1,17 @@
using Asp.Versioning;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Api.Management.Factories;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Services.OperationStatus;
using Umbraco.Cms.Web.Common.Authorization;
namespace Umbraco.Cms.Api.Management.Controllers.DocumentType;
[ApiVersion("1.0")]
[Authorize(Policy = AuthorizationPolicies.TreeAccessDocumentTypes)]
public class ExportDocumentTypeController : DocumentTypeControllerBase
{
private readonly IContentTypeService _contentTypeService;
@@ -1,17 +1,19 @@
using Asp.Versioning;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Api.Management.ViewModels.DocumentType;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.Membership;
using Umbraco.Cms.Core.Security;
using Umbraco.Cms.Core.Services.ImportExport;
using Umbraco.Cms.Core.Services.OperationStatus;
using Umbraco.Cms.Web.Common.Authorization;
namespace Umbraco.Cms.Api.Management.Controllers.DocumentType;
[ApiVersion("1.0")]
[Authorize(Policy = AuthorizationPolicies.TreeAccessDocumentTypes)]
public class ImportExistingDocumentTypeController : DocumentTypeControllerBase
{
private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor;
@@ -1,17 +1,19 @@
using Asp.Versioning;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Api.Management.ViewModels.DocumentType;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.Membership;
using Umbraco.Cms.Core.Security;
using Umbraco.Cms.Core.Services.ImportExport;
using Umbraco.Cms.Core.Services.OperationStatus;
using Umbraco.Cms.Web.Common.Authorization;
namespace Umbraco.Cms.Api.Management.Controllers.DocumentType;
[ApiVersion("1.0")]
[Authorize(Policy = AuthorizationPolicies.TreeAccessDocumentTypes)]
public class ImportNewDocumentTypeController : DocumentTypeControllerBase
{
private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor;
@@ -1,14 +1,17 @@
using Asp.Versioning;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Api.Management.Factories;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Services.OperationStatus;
using Umbraco.Cms.Web.Common.Authorization;
namespace Umbraco.Cms.Api.Management.Controllers.MediaType;
[ApiVersion("1.0")]
[Authorize(Policy = AuthorizationPolicies.TreeAccessMediaTypes)]
public class ExportMediaTypeController : MediaTypeControllerBase
{
private readonly IMediaTypeService _mediaTypeService;
@@ -1,17 +1,19 @@
using Asp.Versioning;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Api.Management.ViewModels.MediaType;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.Membership;
using Umbraco.Cms.Core.Security;
using Umbraco.Cms.Core.Services.ImportExport;
using Umbraco.Cms.Core.Services.OperationStatus;
using Umbraco.Cms.Web.Common.Authorization;
namespace Umbraco.Cms.Api.Management.Controllers.MediaType;
[ApiVersion("1.0")]
[Authorize(Policy = AuthorizationPolicies.TreeAccessMediaTypes)]
public class ImportExistingMediaTypeController : MediaTypeControllerBase
{
private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor;
@@ -1,18 +1,19 @@
using Asp.Versioning;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Api.Management.Controllers.DocumentType;
using Umbraco.Cms.Api.Management.ViewModels.MediaType;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.Membership;
using Umbraco.Cms.Core.Security;
using Umbraco.Cms.Core.Services.ImportExport;
using Umbraco.Cms.Core.Services.OperationStatus;
using Umbraco.Cms.Web.Common.Authorization;
namespace Umbraco.Cms.Api.Management.Controllers.MediaType;
[ApiVersion("1.0")]
[Authorize(Policy = AuthorizationPolicies.TreeAccessMediaTypes)]
public class ImportNewMediaTypeController : MediaTypeControllerBase
{
private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor;
@@ -1,9 +1,8 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Api.Management.Controllers.DocumentType;
using Umbraco.Cms.Api.Management.Routing;
using Umbraco.Cms.Api.Management.ViewModels.MediaType;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Services.OperationStatus;
using Umbraco.Cms.Web.Common.Authorization;
@@ -34,6 +34,8 @@ namespace Umbraco.Cms.Api.Management.Controllers.Security;
[ApiExplorerSettings(IgnoreApi = true)]
public class BackOfficeController : SecurityControllerBase
{
private static long? _loginDurationAverage;
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly IBackOfficeSignInManager _backOfficeSignInManager;
private readonly IBackOfficeUserManager _backOfficeUserManager;
@@ -72,45 +74,65 @@ public class BackOfficeController : SecurityControllerBase
[Authorize(Policy = AuthorizationPolicies.DenyLocalLoginIfConfigured)]
public async Task<IActionResult> Login(CancellationToken cancellationToken, LoginRequestModel model)
{
IdentitySignInResult result = await _backOfficeSignInManager.PasswordSignInAsync(
model.Username, model.Password, true, true);
// Start a timed scope to ensure failed responses return is a consistent time
var loginDuration = Math.Max(_loginDurationAverage ?? _securitySettings.Value.UserDefaultFailedLoginDurationInMilliseconds, _securitySettings.Value.UserMinimumFailedLoginDurationInMilliseconds);
await using var timedScope = new TimedScope(loginDuration, cancellationToken);
if (result.IsNotAllowed)
IdentitySignInResult result = await _backOfficeSignInManager.PasswordSignInAsync(model.Username, model.Password, true, true);
if (result.Succeeded is false)
{
return StatusCode(StatusCodes.Status403Forbidden, new ProblemDetailsBuilder()
.WithTitle("User is not allowed")
.WithDetail("The operation is not allowed on the user")
.Build());
}
if (result.IsLockedOut)
{
return StatusCode(StatusCodes.Status403Forbidden, new ProblemDetailsBuilder()
.WithTitle("User is locked")
.WithDetail("The user is locked, and need to be unlocked before more login attempts can be executed.")
.Build());
}
if(result.RequiresTwoFactor)
{
string? twofactorView = _backOfficeTwoFactorOptions.GetTwoFactorView(model.Username);
BackOfficeIdentityUser? attemptingUser = await _backOfficeUserManager.FindByNameAsync(model.Username);
IEnumerable<string> enabledProviders = (await _userTwoFactorLoginService.GetProviderNamesAsync(attemptingUser!.Key)).Result.Where(x=>x.IsEnabledOnUser).Select(x=>x.ProviderName);
return StatusCode(StatusCodes.Status402PaymentRequired, new RequiresTwoFactorResponseModel()
// TODO: The result should include the user and whether the credentials were valid to avoid these additional checks
BackOfficeIdentityUser? user = await _backOfficeUserManager.FindByNameAsync(model.Username.Trim()); // Align with UmbracoSignInManager and trim username!
if (user is not null &&
await _backOfficeUserManager.CheckPasswordAsync(user, model.Password))
{
TwoFactorLoginView = twofactorView,
EnabledTwoFactorProviderNames = enabledProviders
});
// The credentials were correct, so cancel timed scope and provide a more detailed failure response
await timedScope.CancelAsync();
if (result.IsNotAllowed)
{
return StatusCode(StatusCodes.Status403Forbidden, new ProblemDetailsBuilder()
.WithTitle("User is not allowed")
.WithDetail("The operation is not allowed on the user")
.Build());
}
if (result.IsLockedOut)
{
return StatusCode(StatusCodes.Status403Forbidden, new ProblemDetailsBuilder()
.WithTitle("User is locked")
.WithDetail("The user is locked, and need to be unlocked before more login attempts can be executed.")
.Build());
}
if (result.RequiresTwoFactor)
{
string? twofactorView = _backOfficeTwoFactorOptions.GetTwoFactorView(model.Username);
IEnumerable<string> enabledProviders = (await _userTwoFactorLoginService.GetProviderNamesAsync(user.Key)).Result.Where(x => x.IsEnabledOnUser).Select(x => x.ProviderName);
return StatusCode(StatusCodes.Status402PaymentRequired, new RequiresTwoFactorResponseModel()
{
TwoFactorLoginView = twofactorView,
EnabledTwoFactorProviderNames = enabledProviders
});
}
}
return StatusCode(StatusCodes.Status401Unauthorized, new ProblemDetailsBuilder()
.WithTitle("Invalid credentials")
.WithDetail("The provided credentials are invalid. User has not been signed in.")
.Build());
}
if (result.Succeeded)
{
return Ok();
}
return StatusCode(StatusCodes.Status401Unauthorized, new ProblemDetailsBuilder()
.WithTitle("Invalid credentials")
.WithDetail("The provided credentials are invalid. User has not been signed in.")
.Build());
// Set initial or update average (successful) login duration
_loginDurationAverage = _loginDurationAverage is long average
? (average + (long)timedScope.Elapsed.TotalMilliseconds) / 2
: (long)timedScope.Elapsed.TotalMilliseconds;
// Cancel the timed scope (we don't want to unnecessarily wait on a successful response)
await timedScope.CancelAsync();
return Ok();
}
[AllowAnonymous]
@@ -1,4 +1,4 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Api.Common.Builders;
using Umbraco.Cms.Api.Management.Routing;
@@ -17,6 +17,9 @@ public abstract class TemporaryFileControllerBase : ManagementApiControllerBase
.WithTitle("File extension not allowed")
.WithDetail("The file extension is not allowed.")
.Build()),
TemporaryFileOperationStatus.InvalidFileName => BadRequest(problemDetailsBuilder
.WithTitle("The provided file name is not valid")
.Build()),
TemporaryFileOperationStatus.KeyAlreadyUsed => BadRequest(problemDetailsBuilder
.WithTitle("Key already used")
.WithDetail("The specified key is already used.")
@@ -4,7 +4,7 @@
<Description>Adds imaging support using ImageSharp/ImageSharp.Web version 2 to Umbraco CMS.</Description>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="SixLabors.ImageSharp" VersionOverride="[2.1.9, 3)" />
<PackageReference Include="SixLabors.ImageSharp" VersionOverride="[2.1.10, 3)" />
<PackageReference Include="SixLabors.ImageSharp.Web" VersionOverride="[2.0.2, 3)" />
</ItemGroup>
@@ -11,6 +11,7 @@ public class RepositoryCachePolicyOptions
public RepositoryCachePolicyOptions(Func<int> performCount)
{
PerformCount = performCount;
CacheNullValues = false;
GetAllCacheValidateCount = true;
GetAllCacheAllowZeroCount = false;
}
@@ -21,6 +22,7 @@ public class RepositoryCachePolicyOptions
public RepositoryCachePolicyOptions()
{
PerformCount = null;
CacheNullValues = false;
GetAllCacheValidateCount = false;
GetAllCacheAllowZeroCount = false;
}
@@ -30,6 +32,11 @@ public class RepositoryCachePolicyOptions
/// </summary>
public Func<int>? PerformCount { get; set; }
/// <summary>
/// True if the Get method will cache null results so that the db is not hit for repeated lookups
/// </summary>
public bool CacheNullValues { get; set; }
/// <summary>
/// True/false as to validate the total item count when all items are returned from cache, the default is true but this
/// means that a db lookup will occur - though that lookup will probably be significantly less expensive than the
+1 -1
View File
@@ -34,7 +34,7 @@ public class TypeFinder : ITypeFinder
"ServiceStack.", "SqlCE4Umbraco,", "Superpower,", // used by Serilog
"System.", "TidyNet,", "TidyNet.", "WebDriver,", "itextsharp,", "mscorlib,", "NUnit,", "NUnit.", "NUnit3.",
"Selenium.", "ImageProcessor", "MiniProfiler.", "Owin,", "SQLite",
"ReSharperTestRunner", "ReSharperTestRunner32", "ReSharperTestRunner64", // These are used by the Jetbrains Rider IDE and Visual Studio ReSharper Extension
"ReSharperTestRunner", "ReSharperTestRunner32", "ReSharperTestRunner64", "ReSharperTestRunnerArm32", "ReSharperTestRunnerArm64", // These are used by the Jetbrains Rider IDE and Visual Studio ReSharper Extension
};
private static readonly ConcurrentDictionary<string, Type?> TypeNamesCache = new();
@@ -16,6 +16,7 @@ public class ModelsBuilderSettings
internal const string StaticModelsDirectory = "~/umbraco/models";
internal const bool StaticAcceptUnsafeModelsDirectory = false;
internal const int StaticDebugLevel = 0;
internal const bool StaticIncludeVersionNumberInGeneratedModels = true;
private bool _flagOutOfDateModels = true;
/// <summary>
@@ -78,4 +79,16 @@ public class ModelsBuilderSettings
/// <remarks>0 means minimal (safe on live site), anything else means more and more details (maybe not safe).</remarks>
[DefaultValue(StaticDebugLevel)]
public int DebugLevel { get; set; } = StaticDebugLevel;
/// <summary>
/// Gets or sets a value indicating whether the version number should be included in generated models.
/// </summary>
/// <remarks>
/// By default this is written to the <see cref="System.CodeDom.Compiler.GeneratedCodeAttribute"/> output in
/// generated code for each property of the model. This can be useful for debugging purposes but isn't essential,
/// and it has the causes the generated code to change every time Umbraco is upgraded. In turn, this leads
/// to unnecessary code file changes that need to be checked into source control. Default is <c>true</c>.
/// </remarks>
[DefaultValue(StaticIncludeVersionNumberInGeneratedModels)]
public bool IncludeVersionNumberInGeneratedModels { get; set; } = StaticIncludeVersionNumberInGeneratedModels;
}
@@ -2,6 +2,7 @@
// See LICENSE for more details.
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
namespace Umbraco.Cms.Core.Configuration.Models;
@@ -19,12 +20,16 @@ public class SecuritySettings
internal const bool StaticAllowEditInvariantFromNonDefault = false;
internal const bool StaticAllowConcurrentLogins = false;
internal const string StaticAuthCookieName = "UMB_UCONTEXT";
internal const bool StaticUsernameIsEmail = true;
internal const bool StaticMemberRequireUniqueEmail = true;
internal const string StaticAllowedUserNameCharacters =
"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._@+\\";
internal const int StaticMemberDefaultLockoutTimeInMinutes = 30 * 24 * 60;
internal const int StaticUserDefaultLockoutTimeInMinutes = 30 * 24 * 60;
private const long StaticUserDefaultFailedLoginDurationInMilliseconds = 1000;
private const long StaticUserMinimumFailedLoginDurationInMilliseconds = 250;
internal const string StaticAuthorizeCallbackPathName = "/umbraco/oauth_complete";
internal const string StaticAuthorizeCallbackLogoutPathName = "/umbraco/logout";
internal const string StaticAuthorizeCallbackErrorPathName = "/umbraco/error";
@@ -61,7 +66,14 @@ public class SecuritySettings
/// <summary>
/// Gets or sets a value indicating whether the user's email address is to be considered as their username.
/// </summary>
public bool UsernameIsEmail { get; set; } = true;
[DefaultValue(StaticUsernameIsEmail)]
public bool UsernameIsEmail { get; set; } = StaticUsernameIsEmail;
/// <summary>
/// Gets or sets a value indicating whether the member's email address must be unique.
/// </summary>
[DefaultValue(StaticMemberRequireUniqueEmail)]
public bool MemberRequireUniqueEmail { get; set; } = StaticMemberRequireUniqueEmail;
/// <summary>
/// Gets or sets the set of allowed characters for a username
@@ -108,6 +120,30 @@ public class SecuritySettings
[DefaultValue(StaticAllowConcurrentLogins)]
public bool AllowConcurrentLogins { get; set; } = StaticAllowConcurrentLogins;
/// <summary>
/// Gets or sets the default duration (in milliseconds) of failed login attempts.
/// </summary>
/// <value>
/// The default duration (in milliseconds) of failed login attempts.
/// </value>
/// <remarks>
/// The user login endpoint ensures that failed login attempts take at least as long as the average successful login.
/// However, if no successful logins have occurred, this value is used as the default duration.
/// </remarks>
[Range(0, long.MaxValue)]
[DefaultValue(StaticUserDefaultFailedLoginDurationInMilliseconds)]
public long UserDefaultFailedLoginDurationInMilliseconds { get; set; } = StaticUserDefaultFailedLoginDurationInMilliseconds;
/// <summary>
/// Gets or sets the minimum duration (in milliseconds) of failed login attempts.
/// </summary>
/// <value>
/// The minimum duration (in milliseconds) of failed login attempts.
/// </value>
[Range(0, long.MaxValue)]
[DefaultValue(StaticUserMinimumFailedLoginDurationInMilliseconds)]
public long UserMinimumFailedLoginDurationInMilliseconds { get; set; } = StaticUserMinimumFailedLoginDurationInMilliseconds;
/// <summary>
/// Gets or sets a value of the back-office host URI. Use this when running the back-office client and the Management API on different hosts. Leave empty when running both on the same host.
/// </summary>
@@ -5,7 +5,7 @@
@using Umbraco.Extensions
@{
var isLoggedIn = Context.User?.Identity?.IsAuthenticated ?? false;
var isLoggedIn = Context.User.GetMemberIdentity()?.IsAuthenticated ?? false;
var logoutModel = new PostRedirectModel();
// You can modify this to redirect to a different URL instead of the current one
logoutModel.RedirectUrl = null;
@@ -15,7 +15,7 @@
{
<div class="login-status">
<p>Welcome back <strong>@Context?.User?.Identity?.Name</strong>!</p>
<p>Welcome back <strong>@Context.User?.GetMemberIdentity()?.Name</strong>!</p>
@using (Html.BeginUmbracoForm<UmbLoginStatusController>("HandleLogout", new { RedirectUrl = logoutModel.RedirectUrl }))
{
@@ -0,0 +1,28 @@
namespace Umbraco.Cms.Core.Models;
/// <summary>
/// Describes the options available with publishing a content branch for force publishing.
/// </summary>
[Flags]
public enum PublishBranchFilter
{
/// <summary>
/// The default behavior is to publish only the published content that has changed.
/// </summary>
Default = 0,
/// <summary>
/// For publishing a branch, publish all changed content, including content that is not published.
/// </summary>
IncludeUnpublished = 1,
/// <summary>
/// For publishing a branch, force republishing of all published content, including content that has not changed.
/// </summary>
ForceRepublish = 2,
/// <summary>
/// For publishing a branch, publish all content, including content that is not published and content that has not changed.
/// </summary>
All = IncludeUnpublished | ForceRepublish,
}
@@ -0,0 +1,28 @@
namespace Umbraco.Cms.Core.Models;
/// <summary>
/// Specifies options for publishing notifcations when saving.
/// </summary>
[Flags]
public enum PublishNotificationSaveOptions
{
/// <summary>
/// Do not publish any notifications.
/// </summary>
None = 0,
/// <summary>
/// Only publish the saving notification.
/// </summary>
Saving = 1,
/// <summary>
/// Only publish the saved notification.
/// </summary>
Saved = 2,
/// <summary>
/// Publish all the notifications.
/// </summary>
All = Saving | Saved,
}
+1 -1
View File
@@ -33,7 +33,7 @@ public interface ITagQuery
/// <summary>
/// Gets all document tags.
/// </summary>
/// /// <remarks>
/// <remarks>
/// If no culture is specified, it retrieves tags with an invariant culture.
/// If a culture is specified, it only retrieves tags for that culture.
/// Use "*" to retrieve tags for all cultures.
@@ -216,6 +216,15 @@ public interface IMemberService : IMembershipMemberService, IContentServiceBase<
/// </returns>
IMember CreateMemberWithIdentity(string username, string email, string name, IMemberType memberType);
/// <summary>
/// Saves an <see cref="IMembershipUser" />
/// </summary>
/// <remarks>An <see cref="IMembershipUser" /> can be of type <see cref="IMember" /> or <see cref="IUser" /></remarks>
/// <param name="member"><see cref="IMember" /> or <see cref="IUser" /> to Save</param>
/// <param name="publishNotificationSaveOptions"> Enum for deciding which notifications to publish.</param>
/// <param name="userId">Id of the User saving the Member</param>
Attempt<OperationResult?> Save(IMember member, PublishNotificationSaveOptions publishNotificationSaveOptions, int userId = Constants.Security.SuperUserId) => Save(member, userId);
/// <summary>
/// Saves a single <see cref="IMember" /> object
/// </summary>
@@ -267,6 +276,21 @@ public interface IMemberService : IMembershipMemberService, IContentServiceBase<
/// </returns>
IMember? GetById(int id);
/// <summary>
/// Get an list of <see cref="IMember"/> for all members with the specified email.
/// </summary>
/// <param name="email">Email to use for retrieval</param>
/// <returns>
/// <see cref="IEnumerable{IMember}" />
/// </returns>
IEnumerable<IMember> GetMembersByEmail(string email)
=>
// TODO (V16): Remove this default implementation.
// The following is very inefficient, but will return the correct data, so probably better than throwing a NotImplementedException
// in the default implentation here, for, presumably rare, cases where a custom IMemberService implementation has been registered and
// does not override this method.
GetAllMembers().Where(x => x.Email.Equals(email));
/// <summary>
/// Gets all Members for the specified MemberType alias
/// </summary>
+28 -8
View File
@@ -408,16 +408,23 @@ namespace Umbraco.Cms.Core.Services
}
/// <summary>
/// Get an <see cref="IMember"/> by email
/// Get an <see cref="IMember"/> by email. If RequireUniqueEmailForMembers is set to false, then the first member found with the specified email will be returned.
/// </summary>
/// <param name="email">Email to use for retrieval</param>
/// <returns><see cref="IMember"/></returns>
public IMember? GetByEmail(string email)
public IMember? GetByEmail(string email) => GetMembersByEmail(email).FirstOrDefault();
/// <summary>
/// Get an list of <see cref="IMember"/> for all members with the specified email.
/// </summary>
/// <param name="email">Email to use for retrieval</param>
/// <returns><see cref="IEnumerable{IMember}"/></returns>
public IEnumerable<IMember> GetMembersByEmail(string email)
{
using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true);
scope.ReadLock(Constants.Locks.MemberTree);
IQuery<IMember> query = Query<IMember>().Where(x => x.Email.Equals(email));
return _memberRepository.Get(query)?.FirstOrDefault();
return _memberRepository.Get(query);
}
/// <summary>
@@ -765,6 +772,9 @@ namespace Umbraco.Cms.Core.Services
/// <inheritdoc />
public Attempt<OperationResult?> Save(IMember member, int userId = Constants.Security.SuperUserId)
=> Save(member, PublishNotificationSaveOptions.All, userId);
public Attempt<OperationResult?> Save(IMember member, PublishNotificationSaveOptions publishNotificationSaveOptions, int userId = Constants.Security.SuperUserId)
{
// trimming username and email to make sure we have no trailing space
member.Username = member.Username.Trim();
@@ -773,11 +783,15 @@ namespace Umbraco.Cms.Core.Services
EventMessages evtMsgs = EventMessagesFactory.Get();
using ICoreScope scope = ScopeProvider.CreateCoreScope();
var savingNotification = new MemberSavingNotification(member, evtMsgs);
if (scope.Notifications.PublishCancelable(savingNotification))
MemberSavingNotification? savingNotification = null;
if (publishNotificationSaveOptions.HasFlag(PublishNotificationSaveOptions.Saving))
{
scope.Complete();
return OperationResult.Attempt.Cancel(evtMsgs);
savingNotification = new MemberSavingNotification(member, evtMsgs);
if (scope.Notifications.PublishCancelable(savingNotification))
{
scope.Complete();
return OperationResult.Attempt.Cancel(evtMsgs);
}
}
if (string.IsNullOrWhiteSpace(member.Name))
@@ -789,7 +803,13 @@ namespace Umbraco.Cms.Core.Services
_memberRepository.Save(member);
scope.Notifications.Publish(new MemberSavedNotification(member, evtMsgs).WithStateFrom(savingNotification));
if (publishNotificationSaveOptions.HasFlag(PublishNotificationSaveOptions.Saved))
{
scope.Notifications.Publish(
savingNotification is null
? new MemberSavedNotification(member, evtMsgs)
: new MemberSavedNotification(member, evtMsgs).WithStateFrom(savingNotification));
}
Audit(AuditType.Save, 0, member.Id);
@@ -6,5 +6,6 @@ public enum TemporaryFileOperationStatus
FileExtensionNotAllowed = 1,
KeyAlreadyUsed = 2,
NotFound = 3,
UploadBlocked
UploadBlocked = 4,
InvalidFileName = 5,
}
@@ -45,7 +45,6 @@ internal sealed class TemporaryFileService : ITemporaryFileService
return Attempt.FailWithStatus<TemporaryFileModel?, TemporaryFileOperationStatus>(TemporaryFileOperationStatus.KeyAlreadyUsed, null);
}
await using Stream dataStream = createModel.OpenReadStream();
dataStream.Seek(0, SeekOrigin.Begin);
if (_fileStreamSecurityValidator.IsConsideredSafe(dataStream) is false)
@@ -53,13 +52,12 @@ internal sealed class TemporaryFileService : ITemporaryFileService
return Attempt.FailWithStatus<TemporaryFileModel?, TemporaryFileOperationStatus>(TemporaryFileOperationStatus.UploadBlocked, null);
}
temporaryFileModel = new TemporaryFileModel
{
Key = createModel.Key,
FileName = createModel.FileName,
OpenReadStream = createModel.OpenReadStream,
AvailableUntil = DateTime.Now.Add(_runtimeSettings.TemporaryFileLifeTime)
AvailableUntil = DateTime.Now.Add(_runtimeSettings.TemporaryFileLifeTime),
};
await _temporaryFileRepository.SaveAsync(temporaryFileModel);
@@ -68,17 +66,29 @@ internal sealed class TemporaryFileService : ITemporaryFileService
}
private TemporaryFileOperationStatus Validate(TemporaryFileModelBase temporaryFileModel)
=> IsAllowedFileExtension(temporaryFileModel) == false
? TemporaryFileOperationStatus.FileExtensionNotAllowed
: TemporaryFileOperationStatus.Success;
private bool IsAllowedFileExtension(TemporaryFileModelBase temporaryFileModel)
{
var extension = Path.GetExtension(temporaryFileModel.FileName)[1..];
if (IsAllowedFileExtension(temporaryFileModel.FileName) == false)
{
return TemporaryFileOperationStatus.FileExtensionNotAllowed;
}
if (IsValidFileName(temporaryFileModel.FileName) == false)
{
return TemporaryFileOperationStatus.InvalidFileName;
}
return TemporaryFileOperationStatus.Success;
}
private bool IsAllowedFileExtension(string fileName)
{
var extension = Path.GetExtension(fileName)[1..];
return _contentSettings.IsFileAllowedForUpload(extension);
}
private static bool IsValidFileName(string fileName) =>
!string.IsNullOrEmpty(fileName) && fileName.IndexOfAny(Path.GetInvalidFileNameChars()) < 0;
public async Task<Attempt<TemporaryFileModel?, TemporaryFileOperationStatus>> DeleteAsync(Guid key)
{
TemporaryFileModel? model = await _temporaryFileRepository.GetAsync(key);
@@ -87,6 +87,6 @@ public static class UserServiceExtensions
});
}
[Obsolete("Use IUserService.Get that takes a Guid instead. Scheduled for removal in V15.")]
[Obsolete("Use IUserService.GetAsync that takes a Guid instead. Scheduled for removal in V15.")]
public static IUser? GetByKey(this IUserService userService, Guid key) => userService.GetAsync(key).GetAwaiter().GetResult();
}
+162
View File
@@ -0,0 +1,162 @@
namespace Umbraco.Cms.Core;
/// <summary>
/// Makes a code block timed (take at least a certain amount of time). This class cannot be inherited.
/// </summary>
public sealed class TimedScope : IDisposable, IAsyncDisposable
{
private readonly TimeSpan _duration;
private readonly TimeProvider _timeProvider;
private readonly CancellationTokenSource _cancellationTokenSource;
private readonly long _startingTimestamp;
/// <summary>
/// Gets the elapsed time.
/// </summary>
/// <value>
/// The elapsed time.
/// </value>
public TimeSpan Elapsed
=> _timeProvider.GetElapsedTime(_startingTimestamp);
/// <summary>
/// Gets the remaining time.
/// </summary>
/// <value>
/// The remaining time.
/// </value>
public TimeSpan Remaining
=> TryGetRemaining(out TimeSpan remaining) ? remaining : TimeSpan.Zero;
/// <summary>
/// Initializes a new instance of the <see cref="TimedScope" /> class.
/// </summary>
/// <param name="millisecondsDuration">The number of milliseconds the scope should at least take.</param>
public TimedScope(long millisecondsDuration)
: this(TimeSpan.FromMilliseconds(millisecondsDuration))
{ }
/// <summary>
/// Initializes a new instance of the <see cref="TimedScope" /> class.
/// </summary>
/// <param name="millisecondsDuration">The number of milliseconds the scope should at least take.</param>
/// <param name="cancellationToken">The cancellation token.</param>
public TimedScope(long millisecondsDuration, CancellationToken cancellationToken)
: this(TimeSpan.FromMilliseconds(millisecondsDuration), cancellationToken)
{ }
/// <summary>
/// Initializes a new instance of the <see cref="TimedScope" /> class.
/// </summary>
/// <param name="millisecondsDuration">The number of milliseconds the scope should at least take.</param>
/// <param name="timeProvider">The time provider.</param>
public TimedScope(long millisecondsDuration, TimeProvider timeProvider)
: this(TimeSpan.FromMilliseconds(millisecondsDuration), timeProvider)
{ }
/// <summary>
/// Initializes a new instance of the <see cref="TimedScope" /> class.
/// </summary>
/// <param name="millisecondsDuration">The number of milliseconds the scope should at least take.</param>
/// <param name="timeProvider">The time provider.</param>
/// <param name="cancellationToken">The cancellation token.</param>
public TimedScope(long millisecondsDuration, TimeProvider timeProvider, CancellationToken cancellationToken)
: this(TimeSpan.FromMilliseconds(millisecondsDuration), timeProvider, cancellationToken)
{ }
/// <summary>
/// Initializes a new instance of the <see cref="TimedScope"/> class.
/// </summary>
/// <param name="duration">The duration the scope should at least take.</param>
public TimedScope(TimeSpan duration)
: this(duration, TimeProvider.System)
{ }
/// <summary>
/// Initializes a new instance of the <see cref="TimedScope" /> class.
/// </summary>
/// <param name="duration">The duration the scope should at least take.</param>
/// <param name="timeProvider">The time provider.</param>
public TimedScope(TimeSpan duration, TimeProvider timeProvider)
: this(duration, timeProvider, new CancellationTokenSource())
{ }
/// <summary>
/// Initializes a new instance of the <see cref="TimedScope" /> class.
/// </summary>
/// <param name="duration">The duration the scope should at least take.</param>
/// <param name="cancellationToken">The cancellation token.</param>
public TimedScope(TimeSpan duration, CancellationToken cancellationToken)
: this(duration, TimeProvider.System, cancellationToken)
{ }
/// <summary>
/// Initializes a new instance of the <see cref="TimedScope" /> class.
/// </summary>
/// <param name="duration">The duration the scope should at least take.</param>
/// <param name="timeProvider">The time provider.</param>
/// <param name="cancellationToken">The cancellation token.</param>
public TimedScope(TimeSpan duration, TimeProvider timeProvider, CancellationToken cancellationToken)
: this(duration, timeProvider, CancellationTokenSource.CreateLinkedTokenSource(cancellationToken))
{ }
private TimedScope(TimeSpan duration, TimeProvider timeProvider, CancellationTokenSource cancellationTokenSource)
{
_duration = duration;
_timeProvider = timeProvider;
_cancellationTokenSource = cancellationTokenSource;
_startingTimestamp = timeProvider.GetTimestamp();
}
/// <summary>
/// Cancels the timed scope.
/// </summary>
public void Cancel()
=> _cancellationTokenSource.Cancel();
/// <summary>
/// Cancels the timed scope asynchronously.
/// </summary>
public async Task CancelAsync()
=> await _cancellationTokenSource.CancelAsync().ConfigureAwait(false);
/// <summary>
/// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
/// </summary>
/// <remarks>
/// This will block using <see cref="Thread.Sleep(TimeSpan)" /> until the remaining time has elapsed, if not cancelled.
/// </remarks>
public void Dispose()
{
if (_cancellationTokenSource.IsCancellationRequested is false &&
TryGetRemaining(out TimeSpan remaining))
{
Thread.Sleep(remaining);
}
}
/// <summary>
/// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources asynchronously.
/// </summary>
/// <returns>
/// A task that represents the asynchronous dispose operation.
/// </returns>
/// <remarks>
/// This will delay using <see cref="Task.Delay(TimeSpan, TimeProvider, CancellationToken)" /> until the remaining time has elapsed, if not cancelled.
/// </remarks>
public async ValueTask DisposeAsync()
{
if (_cancellationTokenSource.IsCancellationRequested is false &&
TryGetRemaining(out TimeSpan remaining))
{
await Task.Delay(remaining, _timeProvider, _cancellationTokenSource.Token).ConfigureAwait(false);
}
}
private bool TryGetRemaining(out TimeSpan remaining)
{
remaining = _duration.Subtract(Elapsed);
return remaining > TimeSpan.Zero;
}
}
@@ -24,6 +24,8 @@ public class DefaultRepositoryCachePolicy<TEntity, TId> : RepositoryCachePolicyB
private static readonly TEntity[] _emptyEntities = new TEntity[0]; // const
private readonly RepositoryCachePolicyOptions _options;
private const string NullRepresentationInCache = "*NULL*";
public DefaultRepositoryCachePolicy(IAppPolicyCache cache, IScopeAccessor scopeAccessor, RepositoryCachePolicyOptions options)
: base(cache, scopeAccessor) =>
_options = options ?? throw new ArgumentNullException(nameof(options));
@@ -116,6 +118,7 @@ public class DefaultRepositoryCachePolicy<TEntity, TId> : RepositoryCachePolicyB
{
// whatever happens, clear the cache
var cacheKey = GetEntityCacheKey(entity.Id);
Cache.Clear(cacheKey);
// if there's a GetAllCacheAllowZeroCount cache, ensure it is cleared
@@ -127,20 +130,36 @@ public class DefaultRepositoryCachePolicy<TEntity, TId> : RepositoryCachePolicyB
public override TEntity? Get(TId? id, Func<TId?, TEntity?> performGet, Func<TId[]?, IEnumerable<TEntity>?> performGetAll)
{
var cacheKey = GetEntityCacheKey(id);
TEntity? fromCache = Cache.GetCacheItem<TEntity>(cacheKey);
// if found in cache then return else fetch and cache
if (fromCache != null)
// If found in cache then return immediately.
if (fromCache is not null)
{
return fromCache;
}
// Because TEntity can never be a string, we will never be in a position where the proxy value collides withs a real value.
// Therefore this point can only be reached if there is a proxy null value => becomes null when cast to TEntity above OR the item simply does not exist.
// If we've cached a "null" value, return null.
if (_options.CacheNullValues && Cache.GetCacheItem<string>(cacheKey) == NullRepresentationInCache)
{
return null;
}
// Otherwise go to the database to retrieve.
TEntity? entity = performGet(id);
if (entity != null && entity.HasIdentity)
{
// If we've found an identified entity, cache it for subsequent retrieval.
InsertEntity(cacheKey, entity);
}
else if (entity is null && _options.CacheNullValues)
{
// If we've not found an entity, and we're caching null values, cache a "null" value.
InsertNull(cacheKey);
}
return entity;
}
@@ -248,6 +267,15 @@ public class DefaultRepositoryCachePolicy<TEntity, TId> : RepositoryCachePolicyB
protected virtual void InsertEntity(string cacheKey, TEntity entity)
=> Cache.Insert(cacheKey, () => entity, TimeSpan.FromMinutes(5), true);
protected virtual void InsertNull(string cacheKey)
{
// We can't actually cache a null value, as in doing so wouldn't be able to distinguish between
// a value that does exist but isn't yet cached, or a value that has been explicitly cached with a null value.
// Both would return null when we retrieve from the cache and we couldn't distinguish between the two.
// So we cache a special value that represents null, and then we can check for that value when we retrieve from the cache.
Cache.Insert(cacheKey, () => NullRepresentationInCache, TimeSpan.FromMinutes(5), true);
}
protected virtual void InsertEntities(TId[]? ids, TEntity[]? entities)
{
if (ids?.Length == 0 && entities?.Length == 0 && _options.GetAllCacheAllowZeroCount)
@@ -1,4 +1,4 @@
using NPoco;
using NPoco;
using Umbraco.Cms.Infrastructure.Persistence;
using Umbraco.Cms.Infrastructure.Persistence.Dtos;
using ColumnInfo = Umbraco.Cms.Infrastructure.Persistence.SqlSyntax.ColumnInfo;
@@ -153,16 +153,26 @@ SELECT obj_Constraint.NAME AS 'constraintName'
");
var currentConstraintName = Database.ExecuteScalar<string>(constraintNameQuery);
// only rename the constraint if necessary
// Only rename the constraint if necessary.
if (currentConstraintName == expectedConstraintName)
{
return;
}
Sql<ISqlContext> renameConstraintQuery = Database.SqlContext.Sql(
$"EXEC sp_rename N'{currentConstraintName}', N'{expectedConstraintName}', N'OBJECT'");
Database.Execute(renameConstraintQuery);
if (currentConstraintName is null)
{
// Constraint does not exist, so we need to create it.
Sql<ISqlContext> createConstraintStatement = Database.SqlContext.Sql(@$"
ALTER TABLE umbracoContentVersion ADD CONSTRAINT [DF_umbracoContentVersion_versionDate] DEFAULT (getdate()) FOR [versionDate]");
Database.Execute(createConstraintStatement);
}
else
{
// Constraint exists, and differs from the expected name, so we need to rename it.
Sql<ISqlContext> renameConstraintQuery = Database.SqlContext.Sql(
$"EXEC sp_rename N'{currentConstraintName}', N'{expectedConstraintName}', N'OBJECT'");
Database.Execute(renameConstraintQuery);
}
}
private void UpdateExternalLoginIndexes(IEnumerable<Tuple<string, string, string, bool>> indexes)
@@ -143,14 +143,17 @@ public class TextBuilder : Builder
//
// note that the blog post above clearly states that "Nor should it be applied at the type level if the type being generated is a partial class."
// and since our models are partial classes, we have to apply the attribute against the individual members, not the class itself.
private static void WriteGeneratedCodeAttribute(StringBuilder sb, string tabs) => sb.AppendFormat(
private void WriteGeneratedCodeAttribute(StringBuilder sb, string tabs) => sb.AppendFormat(
"{0}[global::System.CodeDom.Compiler.GeneratedCodeAttribute(\"Umbraco.ModelsBuilder.Embedded\", \"{1}\")]\n",
tabs, ApiVersion.Current.Version);
tabs,
Config.IncludeVersionNumberInGeneratedModels ? ApiVersion.Current.Version : null);
// writes an attribute that specifies that an output may be null.
// (useful for consuming projects with nullable reference types enabled)
private static void WriteMaybeNullAttribute(StringBuilder sb, string tabs, bool isReturn = false) =>
sb.AppendFormat("{0}[{1}global::System.Diagnostics.CodeAnalysis.MaybeNull]\n", tabs,
sb.AppendFormat(
"{0}[{1}global::System.Diagnostics.CodeAnalysis.MaybeNull]\n",
tabs,
isReturn ? "return: " : string.Empty);
private static string MixinStaticGetterName(string clrName) => string.Format("Get{0}", clrName);
@@ -122,11 +122,10 @@ internal class DictionaryRepository : EntityRepositoryBase<int, IDictionaryItem>
var options = new RepositoryCachePolicyOptions
{
// allow zero to be cached
GetAllCacheAllowZeroCount = true,
GetAllCacheAllowZeroCount = true
};
return new SingleItemsOnlyRepositoryCachePolicy<IDictionaryItem, int>(GlobalIsolatedCache, ScopeAccessor,
options);
return new SingleItemsOnlyRepositoryCachePolicy<IDictionaryItem, int>(GlobalIsolatedCache, ScopeAccessor, options);
}
private IDictionaryItem ConvertFromDto(DictionaryDto dto, IDictionary<int, ILanguage> languagesById)
@@ -217,11 +216,10 @@ internal class DictionaryRepository : EntityRepositoryBase<int, IDictionaryItem>
var options = new RepositoryCachePolicyOptions
{
// allow zero to be cached
GetAllCacheAllowZeroCount = true,
GetAllCacheAllowZeroCount = true
};
return new SingleItemsOnlyRepositoryCachePolicy<IDictionaryItem, Guid>(GlobalIsolatedCache, ScopeAccessor,
options);
return new SingleItemsOnlyRepositoryCachePolicy<IDictionaryItem, Guid>(GlobalIsolatedCache, ScopeAccessor, options);
}
protected override IEnumerable<IDictionaryItem> PerformGetAll(params Guid[]? ids)
@@ -272,12 +270,13 @@ internal class DictionaryRepository : EntityRepositoryBase<int, IDictionaryItem>
{
var options = new RepositoryCachePolicyOptions
{
// allow null to be cached
CacheNullValues = true,
// allow zero to be cached
GetAllCacheAllowZeroCount = true,
GetAllCacheAllowZeroCount = true
};
return new SingleItemsOnlyRepositoryCachePolicy<IDictionaryItem, string>(GlobalIsolatedCache, ScopeAccessor,
options);
return new SingleItemsOnlyRepositoryCachePolicy<IDictionaryItem, string>(GlobalIsolatedCache, ScopeAccessor, options);
}
protected override IEnumerable<IDictionaryItem> PerformGetAll(params string[]? ids)
@@ -389,7 +389,9 @@ WHERE r.tagId IS NULL";
}).ToList();
/// <inheritdoc />
public IEnumerable<ITag> GetTagsForEntityType(TaggableObjectTypes objectType, string? group = null,
public IEnumerable<ITag> GetTagsForEntityType(
TaggableObjectTypes objectType,
string? group = null,
string? culture = null)
{
Sql<ISqlContext> sql = GetTagsSql(culture, true);
@@ -403,6 +405,9 @@ WHERE r.tagId IS NULL";
.Where<NodeDto>(dto => dto.NodeObjectType == nodeObjectType);
}
sql = sql
.Where<NodeDto>(dto => !dto.Trashed);
if (group.IsNullOrWhiteSpace() == false)
{
sql = sql
@@ -5,6 +5,7 @@ using HtmlAgilityPack;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Umbraco.Cms.Core.Cache;
using Umbraco.Cms.Core.Configuration.Models;
using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Core.Exceptions;
@@ -33,6 +34,8 @@ public sealed class RichTextEditorPastedImages
private readonly IScopeProvider _scopeProvider;
private readonly IMediaImportService _mediaImportService;
private readonly IImageUrlGenerator _imageUrlGenerator;
private readonly IEntityService _entityService;
private readonly AppCaches _appCaches;
private readonly IUserService _userService;
[Obsolete("Please use the non-obsolete constructor. Will be removed in V16.")]
@@ -84,6 +87,30 @@ public sealed class RichTextEditorPastedImages
{
}
// highest overload to be picked by DI, pointing to newest ctor
[Obsolete("Please use the non-obsolete constructor. Will be removed in V17.")]
public RichTextEditorPastedImages(
IUmbracoContextAccessor umbracoContextAccessor,
ILogger<RichTextEditorPastedImages> logger,
IHostingEnvironment hostingEnvironment,
IMediaService mediaService,
IContentTypeBaseServiceProvider contentTypeBaseServiceProvider,
MediaFileManager mediaFileManager,
MediaUrlGeneratorCollection mediaUrlGenerators,
IShortStringHelper shortStringHelper,
IPublishedUrlProvider publishedUrlProvider,
ITemporaryFileService temporaryFileService,
IScopeProvider scopeProvider,
IMediaImportService mediaImportService,
IImageUrlGenerator imageUrlGenerator,
IOptions<ContentSettings> contentSettings,
IEntityService entityService,
AppCaches appCaches)
: this(umbracoContextAccessor, publishedUrlProvider, temporaryFileService, scopeProvider, mediaImportService, imageUrlGenerator, entityService, appCaches)
{
}
[Obsolete("Please use the non-obsolete constructor. Will be removed in V17.")]
public RichTextEditorPastedImages(
IUmbracoContextAccessor umbracoContextAccessor,
IPublishedUrlProvider publishedUrlProvider,
@@ -91,6 +118,27 @@ public sealed class RichTextEditorPastedImages
IScopeProvider scopeProvider,
IMediaImportService mediaImportService,
IImageUrlGenerator imageUrlGenerator)
: this(
umbracoContextAccessor,
publishedUrlProvider,
temporaryFileService,
scopeProvider,
mediaImportService,
imageUrlGenerator,
StaticServiceProvider.Instance.GetRequiredService<IEntityService>(),
StaticServiceProvider.Instance.GetRequiredService<AppCaches>())
{
}
public RichTextEditorPastedImages(
IUmbracoContextAccessor umbracoContextAccessor,
IPublishedUrlProvider publishedUrlProvider,
ITemporaryFileService temporaryFileService,
IScopeProvider scopeProvider,
IMediaImportService mediaImportService,
IImageUrlGenerator imageUrlGenerator,
IEntityService entityService,
AppCaches appCaches)
{
_umbracoContextAccessor =
umbracoContextAccessor ?? throw new ArgumentNullException(nameof(umbracoContextAccessor));
@@ -99,6 +147,8 @@ public sealed class RichTextEditorPastedImages
_scopeProvider = scopeProvider;
_mediaImportService = mediaImportService;
_imageUrlGenerator = imageUrlGenerator;
_entityService = entityService;
_appCaches = appCaches;
// this obviously is not correct. however, we only use IUserService in an obsolete method,
// so this is better than having even more obsolete constructors for V16
@@ -161,7 +211,7 @@ public sealed class RichTextEditorPastedImages
if (uploadedImages.ContainsKey(temporaryFileKey) == false)
{
using Stream fileStream = temporaryFile.OpenReadStream();
Guid? parentFolderKey = mediaParentFolder == Guid.Empty ? Constants.System.RootKey : mediaParentFolder;
Guid? parentFolderKey = mediaParentFolder == Guid.Empty ? await GetDefaultMediaRoot(userKey) : mediaParentFolder;
IMedia mediaFile = await _mediaImportService.ImportAsync(temporaryFile.FileName, fileStream, parentFolderKey, MediaTypeAlias(temporaryFile.FileName), userKey);
udi = mediaFile.GetUdi();
}
@@ -214,6 +264,20 @@ public sealed class RichTextEditorPastedImages
return htmlDoc.DocumentNode.OuterHtml;
}
private async Task<Guid?> GetDefaultMediaRoot(Guid userKey)
{
IUser user = await _userService.GetAsync(userKey) ?? throw new ArgumentException("User could not be found");
var userStartNodes = user.CalculateMediaStartNodeIds(_entityService, _appCaches);
var firstNodeId = userStartNodes?.FirstOrDefault();
if (firstNodeId is null)
{
return Constants.System.RootKey;
}
Attempt<Guid> firstNodeKeyAttempt = _entityService.GetKey(firstNodeId.Value, UmbracoObjectTypes.Media);
return firstNodeKeyAttempt.Success ? firstNodeKeyAttempt.Result : Constants.System.RootKey;
}
private string MediaTypeAlias(string fileName)
=> fileName.InvariantEndsWith(".svg")
? Constants.Conventions.MediaTypes.VectorGraphicsAlias
@@ -15,6 +15,7 @@ internal class FileSystemMainDomLock : IMainDomLock
private readonly string _lockFilePath;
private readonly ILogger<FileSystemMainDomLock> _logger;
private readonly string _releaseSignalFilePath;
private bool _disposed;
private Task? _listenForReleaseSignalFileTask;
private FileStream? _lockFileStream;
@@ -89,16 +90,14 @@ internal class FileSystemMainDomLock : IMainDomLock
ListeningLoop,
_cancellationTokenSource.Token,
TaskCreationOptions.LongRunning,
TaskScheduler.Default);
TaskScheduler.Default)
.Unwrap(); // Because ListeningLoop is an async method, we need to use Unwrap to return the inner task.
return _listenForReleaseSignalFileTask;
}
public void Dispose()
{
_lockFileStream?.Close();
_lockFileStream = null;
}
/// <summary>Releases the resources used by this <see cref="FileSystemMainDomLock" />.</summary>
public void Dispose() => Dispose(true);
public void CreateLockReleaseSignalFile() =>
File.Open(_releaseSignalFilePath, FileMode.OpenOrCreate, FileAccess.ReadWrite,
@@ -108,7 +107,27 @@ internal class FileSystemMainDomLock : IMainDomLock
public void DeleteLockReleaseSignalFile() =>
File.Delete(_releaseSignalFilePath);
private void ListeningLoop()
/// <summary>Releases the resources used by this <see cref="FileSystemMainDomLock" />.</summary>
/// <param name="disposing">true to release both managed resources.</param>
protected virtual void Dispose(bool disposing)
{
if (disposing && !_disposed)
{
_logger.LogInformation($"{nameof(FileSystemMainDomLock)} Disposing...");
_cancellationTokenSource.Cancel();
_cancellationTokenSource.Dispose();
ReleaseLock();
_disposed = true;
}
}
private void ReleaseLock()
{
_lockFileStream?.Close();
_lockFileStream = null;
}
private async Task ListeningLoop()
{
while (true)
{
@@ -127,12 +146,12 @@ internal class FileSystemMainDomLock : IMainDomLock
{
_logger.LogDebug("Found lock release signal file, releasing lock on {lockFilePath}", _lockFilePath);
}
_lockFileStream?.Close();
_lockFileStream = null;
ReleaseLock();
break;
}
Thread.Sleep(_globalSettings.CurrentValue.MainDomReleaseSignalPollingInterval);
await Task.Delay(_globalSettings.CurrentValue.MainDomReleaseSignalPollingInterval, _cancellationTokenSource.Token);
}
}
}
@@ -128,7 +128,7 @@ public class MemberUserStore : UmbracoUserStore<MemberIdentityUser, UmbracoIdent
UpdateMemberProperties(memberEntity, user, out bool _);
// create the member
Attempt<OperationResult?> saveAttempt = _memberService.Save(memberEntity);
Attempt<OperationResult?> saveAttempt = _memberService.Save(memberEntity, PublishNotificationSaveOptions.Saving);
if (saveAttempt.Success is false)
{
scope.Complete();
@@ -35,18 +35,29 @@ public abstract class UmbracoUserStore<TUser, TRole>
[Obsolete("Use TryConvertIdentityIdToInt instead. Scheduled for removal in V15.")]
protected static int UserIdToInt(string? userId)
{
if (int.TryParse(userId, NumberStyles.Integer, CultureInfo.InvariantCulture, out var result))
if (TryUserIdToInt(userId, out int result))
{
return result;
}
throw new InvalidOperationException($"Unable to convert user ID ({userId})to int using InvariantCulture");
}
protected static bool TryUserIdToInt(string? userId, out int result)
{
if (int.TryParse(userId, NumberStyles.Integer, CultureInfo.InvariantCulture, out result))
{
return true;
}
if (Guid.TryParse(userId, out Guid key))
{
// Reverse the IntExtensions.ToGuid
return BitConverter.ToInt32(key.ToByteArray(), 0);
result = BitConverter.ToInt32(key.ToByteArray(), 0);
return true;
}
throw new InvalidOperationException($"Unable to convert user ID ({userId})to int using InvariantCulture");
return false;
}
protected abstract Task<int> ResolveEntityIdFromIdentityId(string? identityId);
@@ -62,9 +62,16 @@ public static class HttpContextExtensions
// Update the HttpContext's user with the authenticated user's principal to ensure
// that subsequent requests within the same context will recognize the user
// as authenticated.
if (result.Succeeded)
if (result is { Succeeded: true, Principal.Identity: not null })
{
httpContext.User = result.Principal;
// We need to get existing identities that are not the backoffice kind and flow them to the new identity
// Otherwise we can't log in as both a member and a backoffice user
// For instance if you've enabled basic auth.
ClaimsPrincipal? authenticatedPrincipal = result.Principal;
IEnumerable<ClaimsIdentity> existingIdentities = httpContext.User.Identities.Where(x => x.IsAuthenticated && x.AuthenticationType != authenticatedPrincipal.Identity.AuthenticationType);
authenticatedPrincipal.AddIdentities(existingIdentities);
httpContext.User = authenticatedPrincipal;
}
return result;
@@ -0,0 +1,18 @@
using System.Security.Claims;
using Microsoft.AspNetCore.Identity;
namespace Umbraco.Extensions;
public static class MemberClaimsPrincipalExtensions
{
/// <summary>
/// Tries to get specifically the member identity from the ClaimsPrincipal
/// </summary>
/// <remarks>
/// The identity returned is the one with default authentication type.
/// </remarks>
/// <param name="principal">The principal to find the identity in.</param>
/// <returns>The default authenticated authentication type identity.</returns>
public static ClaimsIdentity? GetMemberIdentity(this ClaimsPrincipal principal)
=> principal.Identities.FirstOrDefault(x => x.AuthenticationType == IdentityConstants.ApplicationScheme);
}
@@ -24,7 +24,7 @@ public sealed class ConfigureMemberIdentityOptions : IConfigureOptions<IdentityO
options.SignIn.RequireConfirmedEmail = false; // not implemented
options.SignIn.RequireConfirmedPhoneNumber = false; // not implemented
options.User.RequireUniqueEmail = true;
options.User.RequireUniqueEmail = _securitySettings.MemberRequireUniqueEmail;
// Support validation of member names using Down-Level Logon Name format
options.User.AllowedUserNameCharacters = _securitySettings.AllowedUserNameCharacters;
@@ -1,4 +1,5 @@
using System.Globalization;
using System.Security.Claims;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Logging;
@@ -113,8 +114,11 @@ public class MemberManager : UmbracoUserManager<MemberIdentityUser, MemberPasswo
/// <inheritdoc />
public virtual bool IsLoggedIn()
{
HttpContext? httpContext = _httpContextAccessor.HttpContext;
return httpContext?.User.Identity?.IsAuthenticated ?? false;
// We have to try and specifically find the member identity, it's entirely possible for there to be both backoffice and member.
ClaimsIdentity? memberIdentity = _httpContextAccessor.HttpContext?.User.GetMemberIdentity();
return memberIdentity is not null &&
memberIdentity.IsAuthenticated;
}
/// <inheritdoc />
@@ -170,23 +174,27 @@ public class MemberManager : UmbracoUserManager<MemberIdentityUser, MemberPasswo
/// <inheritdoc />
public virtual async Task<MemberIdentityUser?> GetCurrentMemberAsync()
{
if (_currentMember == null)
if (_currentMember is not null)
{
if (!IsLoggedIn())
{
return null;
}
_currentMember = await GetUserAsync(_httpContextAccessor.HttpContext?.User!);
return _currentMember;
}
if (IsLoggedIn() is false)
{
return null;
}
// Create a principal the represents the member security context.
var memberPrincipal = new ClaimsPrincipal(_httpContextAccessor.HttpContext?.User.GetMemberIdentity()!);
_currentMember = await GetUserAsync(memberPrincipal);
return _currentMember;
}
public virtual IPublishedContent? AsPublishedMember(MemberIdentityUser user) => _store.GetPublishedMember(user);
/// <summary>
/// This will check if the member has access to this path
/// This will check if the member has access to this path.
/// </summary>
/// <param name="path"></param>
/// <returns></returns>
@@ -141,7 +141,10 @@ public abstract class UmbracoViewPage<TModel> : RazorPage<TModel>
string.Format(
ContentSettings.PreviewBadge,
HostingEnvironment.ToAbsolute(GlobalSettings.UmbracoPath),
Context.Request.GetEncodedUrl(),
System.Web.HttpUtility.HtmlEncode(Context.Request.GetEncodedUrl()), // Belt and braces - via a browser at least it doesn't seem possible to have anything other than
// a valid culture code provided in the querystring of this URL.
// But just to be sure of prevention of an XSS vulnterablity we'll HTML encode here too.
// An expected URL is untouched by this encoding.
UmbracoContext.PublishedRequest?.PublishedContent?.Key);
}
else
@@ -0,0 +1,86 @@
using Microsoft.Extensions.DependencyInjection;
using NUnit.Framework;
using Umbraco.Cms.Core.Configuration.Models;
using Umbraco.Cms.Core.Models.TemporaryFile;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Services.OperationStatus;
using Umbraco.Cms.Tests.Common.Testing;
using Umbraco.Cms.Tests.Integration.Testing;
namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.Services;
[TestFixture]
[UmbracoTest(Database = UmbracoTestOptions.Database.NewSchemaPerFixture)]
public class TemporaryFileServiceTests : UmbracoIntegrationTest
{
private ITemporaryFileService TemporaryFileService => GetRequiredService<ITemporaryFileService>();
protected override void CustomTestSetup(IUmbracoBuilder builder) =>
builder.Services.Configure<ContentSettings>(config =>
config.AllowedUploadedFileExtensions = ["txt"]);
[Test]
public async Task Can_Create_Get_And_Delete_Temporary_File()
{
var key = Guid.NewGuid();
const string FileName = "test.txt";
const string FileContents = "test";
var model = new CreateTemporaryFileModel
{
FileName = FileName,
Key = key,
OpenReadStream = () =>
{
var stream = new MemoryStream();
var writer = new StreamWriter(stream);
writer.Write(FileContents);
writer.Flush();
stream.Position = 0;
return stream;
}
};
var createAttempt = await TemporaryFileService.CreateAsync(model);
Assert.IsTrue(createAttempt.Success);
TemporaryFileModel? fileModel = await TemporaryFileService.GetAsync(key);
Assert.IsNotNull(fileModel);
Assert.AreEqual(key, fileModel.Key);
Assert.AreEqual(FileName, fileModel.FileName);
using (var reader = new StreamReader(fileModel.OpenReadStream()))
{
string fileContents = reader.ReadToEnd();
Assert.AreEqual(FileContents, fileContents);
}
var deleteAttempt = await TemporaryFileService.DeleteAsync(key);
Assert.IsTrue(createAttempt.Success);
fileModel = await TemporaryFileService.GetAsync(key);
Assert.IsNull(fileModel);
}
[Test]
public async Task Cannot_Create_File_Outside_Of_Temporary_Files_Root()
{
var key = Guid.NewGuid();
const string FileName = "../test.txt";
var model = new CreateTemporaryFileModel
{
FileName = FileName,
Key = key,
OpenReadStream = () =>
{
var stream = new MemoryStream();
var writer = new StreamWriter(stream);
writer.Write(string.Empty);
writer.Flush();
stream.Position = 0;
return stream;
}
};
var createAttempt = await TemporaryFileService.CreateAsync(model);
Assert.IsFalse(createAttempt.Success);
Assert.AreEqual(TemporaryFileOperationStatus.InvalidFileName, createAttempt.Status);
}
}
@@ -638,6 +638,89 @@ public class TagRepositoryTest : UmbracoIntegrationTest
}
}
[Test]
public void Can_Get_Tags_For_Entity_Type_Excluding_Trashed_Entity()
{
var provider = ScopeProvider;
using (ScopeProvider.CreateScope())
{
var template = TemplateBuilder.CreateTextPageTemplate();
FileService.SaveTemplate(template);
var contentType = ContentTypeBuilder.CreateSimpleContentType("test", "Test", defaultTemplateId: template.Id);
ContentTypeRepository.Save(contentType);
var content1 = ContentBuilder.CreateSimpleContent(contentType);
content1.PublishCulture(CultureImpact.Invariant);
content1.PublishedState = PublishedState.Publishing;
DocumentRepository.Save(content1);
var content2 = ContentBuilder.CreateSimpleContent(contentType);
content2.PublishCulture(CultureImpact.Invariant);
content2.PublishedState = PublishedState.Publishing;
content2.Trashed = true;
DocumentRepository.Save(content2);
var mediaType = MediaTypeBuilder.CreateImageMediaType("image2");
MediaTypeRepository.Save(mediaType);
var media1 = MediaBuilder.CreateMediaImage(mediaType, -1);
MediaRepository.Save(media1);
var media2 = MediaBuilder.CreateMediaImage(mediaType, -1);
media2.Trashed = true;
MediaRepository.Save(media2);
var repository = CreateRepository(provider);
Tag[] tags =
{
new Tag {Text = "tag1", Group = "test"},
new Tag {Text = "tag2", Group = "test1"},
new Tag {Text = "tag3", Group = "test"}
};
Tag[] tags2 =
{
new Tag {Text = "tag4", Group = "test"},
new Tag {Text = "tag5", Group = "test1"},
new Tag {Text = "tag6", Group = "test"}
};
repository.Assign(
content1.Id,
contentType.PropertyTypes.First().Id,
tags,
false);
repository.Assign(
content2.Id,
contentType.PropertyTypes.First().Id,
tags2,
false);
repository.Assign(
media1.Id,
contentType.PropertyTypes.First().Id,
tags,
false);
repository.Assign(
media2.Id,
contentType.PropertyTypes.First().Id,
tags2,
false);
var result1 = repository.GetTagsForEntityType(TaggableObjectTypes.Content).ToArray();
var result2 = repository.GetTagsForEntityType(TaggableObjectTypes.Media).ToArray();
var result3 = repository.GetTagsForEntityType(TaggableObjectTypes.All).ToArray();
const string ExpectedTags = "tag1,tag2,tag3";
Assert.AreEqual(ExpectedTags, string.Join(",", result1.Select(x => x.Text)));
Assert.AreEqual(ExpectedTags, string.Join(",", result2.Select(x => x.Text)));
Assert.AreEqual(ExpectedTags, string.Join(",", result3.Select(x => x.Text)));
}
}
[Test]
public void Can_Get_Tags_For_Entity_Type()
{
@@ -268,7 +268,7 @@ public class MemberManagerTests
.Setup(x => x.CreateMember(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>()))
.Returns(fakeMember);
_mockMemberService
.Setup(x => x.Save(fakeMember, Constants.Security.SuperUserId))
.Setup(x => x.Save(fakeMember, It.IsAny<PublishNotificationSaveOptions>(), Constants.Security.SuperUserId))
.Returns(Attempt.Succeed<OperationResult?>(null));
}
@@ -124,7 +124,7 @@ public class MemberUserStoreTests
.Setup(x => x.CreateMember(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>()))
.Returns(mockMember);
_mockMemberService
.Setup(x => x.Save(mockMember, Constants.Security.SuperUserId))
.Setup(x => x.Save(mockMember, PublishNotificationSaveOptions.Saving, Constants.Security.SuperUserId))
.Returns(Attempt.Succeed<OperationResult?>(null));
// act
var identityResult = await sut.CreateAsync(fakeUser, CancellationToken.None);
@@ -134,7 +134,7 @@ public class MemberUserStoreTests
Assert.IsTrue(!identityResult.Errors.Any());
_mockMemberService.Verify(x =>
x.CreateMember(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>()));
_mockMemberService.Verify(x => x.Save(mockMember, Constants.Security.SuperUserId));
_mockMemberService.Verify(x => x.Save(mockMember, PublishNotificationSaveOptions.Saving, Constants.Security.SuperUserId));
}
[Test]
+5 -7
View File
@@ -71,25 +71,23 @@ internal class UmbracoCmsSchema
public required HelpPageSettings HelpPage { get; set; }
public required InstallDefaultDataSettings DefaultDataCreation { get; set; }
public required InstallDefaultDataNamedOptions InstallDefaultData { get; set; }
public required DataTypesSettings DataTypes { get; set; }
public required MarketplaceSettings Marketplace { get; set; }
public InstallDefaultDataNamedOptions InstallDefaultData { get; set; } = null!;
public required WebhookSettings Webhook { get; set; }
}
public class InstallDefaultDataNamedOptions
{
public InstallDefaultDataSettings Languages { get; set; } = null!;
public required InstallDefaultDataSettings Languages { get; set; }
public InstallDefaultDataSettings DataTypes { get; set; } = null!;
public required InstallDefaultDataSettings DataTypes { get; set; }
public InstallDefaultDataSettings MediaTypes { get; set; } = null!;
public required InstallDefaultDataSettings MediaTypes { get; set; }
public InstallDefaultDataSettings MemberTypes { get; set; } = null!;
public required InstallDefaultDataSettings MemberTypes { get; set; }
}
}