Skip to content

Commit 9e7a368

Browse files
committed
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
2 parents 8056a59 + db1d999 commit 9e7a368

File tree

29 files changed

+479
-81
lines changed

29 files changed

+479
-81
lines changed

src/Umbraco.Cms.Api.Delivery/Services/RequestRedirectService.cs

Lines changed: 26 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -42,31 +42,42 @@ public RequestRedirectService(
4242
{
4343
requestedPath = requestedPath.EnsureStartsWith("/");
4444

45+
IPublishedContent? startItem = GetStartItem();
46+
4547
// must append the root content url segment if it is not hidden by config, because
4648
// the URL tracking is based on the actual URL, including the root content url segment
47-
if (_globalSettings.HideTopLevelNodeFromPath == false)
49+
if (_globalSettings.HideTopLevelNodeFromPath == false && startItem?.UrlSegment != null)
4850
{
49-
IPublishedContent? startItem = GetStartItem();
50-
if (startItem?.UrlSegment != null)
51-
{
52-
requestedPath = $"{startItem.UrlSegment.EnsureStartsWith("/")}{requestedPath}";
53-
}
51+
requestedPath = $"{startItem.UrlSegment.EnsureStartsWith("/")}{requestedPath}";
5452
}
5553

5654
var culture = _requestCultureService.GetRequestedCulture();
5755

58-
// append the configured domain content ID to the path if we have a domain bound request,
59-
// because URL tracking registers the tracked url like "{domain content ID}/{content path}"
60-
Uri contentRoute = GetDefaultRequestUri(requestedPath);
61-
DomainAndUri? domainAndUri = GetDomainAndUriForRoute(contentRoute);
62-
if (domainAndUri != null)
56+
// important: redirect URLs are always tracked without trailing slashes
57+
requestedPath = requestedPath.TrimEnd("/");
58+
IRedirectUrl? redirectUrl = _redirectUrlService.GetMostRecentRedirectUrl(requestedPath, culture);
59+
60+
// if a redirect URL was not found, try by appending the start item ID because URL tracking might have tracked
61+
// a redirect with "{root content ID}/{content path}"
62+
if (redirectUrl is null && startItem is not null)
6363
{
64-
requestedPath = GetContentRoute(domainAndUri, contentRoute);
65-
culture ??= domainAndUri.Culture;
64+
redirectUrl = _redirectUrlService.GetMostRecentRedirectUrl($"{startItem.Id}{requestedPath}", culture);
65+
}
66+
67+
// still no redirect URL found - try looking for a configured domain if we have a domain bound request,
68+
// because URL tracking might have tracked a redirect with "{domain content ID}/{content path}"
69+
if (redirectUrl is null)
70+
{
71+
Uri contentRoute = GetDefaultRequestUri(requestedPath);
72+
DomainAndUri? domainAndUri = GetDomainAndUriForRoute(contentRoute);
73+
if (domainAndUri is not null)
74+
{
75+
requestedPath = GetContentRoute(domainAndUri, contentRoute);
76+
culture ??= domainAndUri.Culture;
77+
redirectUrl = _redirectUrlService.GetMostRecentRedirectUrl(requestedPath, culture);
78+
}
6679
}
6780

68-
// important: redirect URLs are always tracked without trailing slashes
69-
IRedirectUrl? redirectUrl = _redirectUrlService.GetMostRecentRedirectUrl(requestedPath.TrimEnd("/"), culture);
7081
IPublishedContent? content = redirectUrl != null
7182
? _apiPublishedContentCache.GetById(redirectUrl.ContentKey)
7283
: null;

src/Umbraco.Core/Cache/RepositoryCachePolicyOptions.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ public class RepositoryCachePolicyOptions
1111
public RepositoryCachePolicyOptions(Func<int> performCount)
1212
{
1313
PerformCount = performCount;
14+
CacheNullValues = false;
1415
GetAllCacheValidateCount = true;
1516
GetAllCacheAllowZeroCount = false;
1617
}
@@ -21,6 +22,7 @@ public RepositoryCachePolicyOptions(Func<int> performCount)
2122
public RepositoryCachePolicyOptions()
2223
{
2324
PerformCount = null;
25+
CacheNullValues = false;
2426
GetAllCacheValidateCount = false;
2527
GetAllCacheAllowZeroCount = false;
2628
}
@@ -30,6 +32,11 @@ public RepositoryCachePolicyOptions()
3032
/// </summary>
3133
public Func<int>? PerformCount { get; set; }
3234

35+
/// <summary>
36+
/// True if the Get method will cache null results so that the db is not hit for repeated lookups
37+
/// </summary>
38+
public bool CacheNullValues { get; set; }
39+
3340
/// <summary>
3441
/// True/false as to validate the total item count when all items are returned from cache, the default is true but this
3542
/// means that a db lookup will occur - though that lookup will probably be significantly less expensive than the

src/Umbraco.Core/Composing/TypeFinder.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ public class TypeFinder : ITypeFinder
3434
"ServiceStack.", "SqlCE4Umbraco,", "Superpower,", // used by Serilog
3535
"System.", "TidyNet,", "TidyNet.", "WebDriver,", "itextsharp,", "mscorlib,", "NUnit,", "NUnit.", "NUnit3.",
3636
"Selenium.", "ImageProcessor", "MiniProfiler.", "Owin,", "SQLite",
37-
"ReSharperTestRunner", "ReSharperTestRunner32", "ReSharperTestRunner64", // These are used by the Jetbrains Rider IDE and Visual Studio ReSharper Extension
37+
"ReSharperTestRunner", "ReSharperTestRunner32", "ReSharperTestRunner64", "ReSharperTestRunnerArm32", "ReSharperTestRunnerArm64", // These are used by the Jetbrains Rider IDE and Visual Studio ReSharper Extension
3838
};
3939

4040
private static readonly ConcurrentDictionary<string, Type?> TypeNamesCache = new();

src/Umbraco.Core/Configuration/Models/ModelsBuilderSettings.cs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ public class ModelsBuilderSettings
1616
internal const string StaticModelsDirectory = "~/umbraco/models";
1717
internal const bool StaticAcceptUnsafeModelsDirectory = false;
1818
internal const int StaticDebugLevel = 0;
19+
internal const bool StaticIncludeVersionNumberInGeneratedModels = true;
1920
private bool _flagOutOfDateModels = true;
2021

2122
/// <summary>
@@ -78,4 +79,16 @@ public bool FlagOutOfDateModels
7879
/// <remarks>0 means minimal (safe on live site), anything else means more and more details (maybe not safe).</remarks>
7980
[DefaultValue(StaticDebugLevel)]
8081
public int DebugLevel { get; set; } = StaticDebugLevel;
82+
83+
/// <summary>
84+
/// Gets or sets a value indicating whether the version number should be included in generated models.
85+
/// </summary>
86+
/// <remarks>
87+
/// By default this is written to the <see cref="System.CodeDom.Compiler.GeneratedCodeAttribute"/> output in
88+
/// generated code for each property of the model. This can be useful for debugging purposes but isn't essential,
89+
/// and it has the causes the generated code to change every time Umbraco is upgraded. In turn, this leads
90+
/// to unnecessary code file changes that need to be checked into source control. Default is <c>true</c>.
91+
/// </remarks>
92+
[DefaultValue(StaticIncludeVersionNumberInGeneratedModels)]
93+
public bool IncludeVersionNumberInGeneratedModels { get; set; } = StaticIncludeVersionNumberInGeneratedModels;
8194
}

src/Umbraco.Core/Configuration/Models/SecuritySettings.cs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ public class SecuritySettings
1919
internal const bool StaticAllowEditInvariantFromNonDefault = false;
2020
internal const bool StaticAllowConcurrentLogins = false;
2121
internal const string StaticAuthCookieName = "UMB_UCONTEXT";
22+
internal const bool StaticUsernameIsEmail = true;
23+
internal const bool StaticMemberRequireUniqueEmail = true;
2224

2325
internal const string StaticAllowedUserNameCharacters =
2426
"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._@+\\";
@@ -61,7 +63,14 @@ public class SecuritySettings
6163
/// <summary>
6264
/// Gets or sets a value indicating whether the user's email address is to be considered as their username.
6365
/// </summary>
64-
public bool UsernameIsEmail { get; set; } = true;
66+
[DefaultValue(StaticUsernameIsEmail)]
67+
public bool UsernameIsEmail { get; set; } = StaticUsernameIsEmail;
68+
69+
/// <summary>
70+
/// Gets or sets a value indicating whether the member's email address must be unique.
71+
/// </summary>
72+
[DefaultValue(StaticMemberRequireUniqueEmail)]
73+
public bool MemberRequireUniqueEmail { get; set; } = StaticMemberRequireUniqueEmail;
6574

6675
/// <summary>
6776
/// Gets or sets the set of allowed characters for a username

src/Umbraco.Core/EmbeddedResources/Snippets/LoginStatus.cshtml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
@using Umbraco.Extensions
66

77
@{
8-
var isLoggedIn = Context.User?.Identity?.IsAuthenticated ?? false;
8+
var isLoggedIn = Context.User.GetMemberIdentity()?.IsAuthenticated ?? false;
99
var logoutModel = new PostRedirectModel();
1010
// You can modify this to redirect to a different URL instead of the current one
1111
logoutModel.RedirectUrl = null;
@@ -15,7 +15,7 @@
1515
{
1616
<div class="login-status">
1717

18-
<p>Welcome back <strong>@Context?.User?.Identity?.Name</strong>!</p>
18+
<p>Welcome back <strong>@Context.User?.GetMemberIdentity()?.Name</strong>!</p>
1919

2020
@using (Html.BeginUmbracoForm<UmbLoginStatusController>("HandleLogout", new { RedirectUrl = logoutModel.RedirectUrl }))
2121
{
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
namespace Umbraco.Cms.Core.Models;
2+
3+
/// <summary>
4+
/// Describes the options available with publishing a content branch for force publishing.
5+
/// </summary>
6+
[Flags]
7+
public enum PublishBranchFilter
8+
{
9+
/// <summary>
10+
/// The default behavior is to publish only the published content that has changed.
11+
/// </summary>
12+
Default = 0,
13+
14+
/// <summary>
15+
/// For publishing a branch, publish all changed content, including content that is not published.
16+
/// </summary>
17+
IncludeUnpublished = 1,
18+
19+
/// <summary>
20+
/// For publishing a branch, force republishing of all published content, including content that has not changed.
21+
/// </summary>
22+
ForceRepublish = 2,
23+
24+
/// <summary>
25+
/// For publishing a branch, publish all content, including content that is not published and content that has not changed.
26+
/// </summary>
27+
All = IncludeUnpublished | ForceRepublish,
28+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
namespace Umbraco.Cms.Core.Models;
2+
3+
/// <summary>
4+
/// Specifies options for publishing notifcations when saving.
5+
/// </summary>
6+
[Flags]
7+
public enum PublishNotificationSaveOptions
8+
{
9+
/// <summary>
10+
/// Do not publish any notifications.
11+
/// </summary>
12+
None = 0,
13+
14+
/// <summary>
15+
/// Only publish the saving notification.
16+
/// </summary>
17+
Saving = 1,
18+
19+
/// <summary>
20+
/// Only publish the saved notification.
21+
/// </summary>
22+
Saved = 2,
23+
24+
/// <summary>
25+
/// Publish all the notifications.
26+
/// </summary>
27+
All = Saving | Saved,
28+
}

src/Umbraco.Core/PublishedCache/ITagQuery.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ public interface ITagQuery
3333
/// <summary>
3434
/// Gets all document tags.
3535
/// </summary>
36-
/// /// <remarks>
36+
/// <remarks>
3737
/// If no culture is specified, it retrieves tags with an invariant culture.
3838
/// If a culture is specified, it only retrieves tags for that culture.
3939
/// Use "*" to retrieve tags for all cultures.

src/Umbraco.Core/Services/IMemberService.cs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,15 @@ IMember CreateMemberWithIdentity(string username, string email, string name, str
216216
/// </returns>
217217
IMember CreateMemberWithIdentity(string username, string email, string name, IMemberType memberType);
218218

219+
/// <summary>
220+
/// Saves an <see cref="IMembershipUser" />
221+
/// </summary>
222+
/// <remarks>An <see cref="IMembershipUser" /> can be of type <see cref="IMember" /> or <see cref="IUser" /></remarks>
223+
/// <param name="member"><see cref="IMember" /> or <see cref="IUser" /> to Save</param>
224+
/// <param name="publishNotificationSaveOptions"> Enum for deciding which notifications to publish.</param>
225+
/// <param name="userId">Id of the User saving the Member</param>
226+
Attempt<OperationResult?> Save(IMember member, PublishNotificationSaveOptions publishNotificationSaveOptions, int userId = Constants.Security.SuperUserId) => Save(member, userId);
227+
219228
/// <summary>
220229
/// Saves a single <see cref="IMember" /> object
221230
/// </summary>
@@ -267,6 +276,21 @@ IMember CreateMemberWithIdentity(string username, string email, string name, str
267276
/// </returns>
268277
IMember? GetById(int id);
269278

279+
/// <summary>
280+
/// Get an list of <see cref="IMember"/> for all members with the specified email.
281+
/// </summary>
282+
/// <param name="email">Email to use for retrieval</param>
283+
/// <returns>
284+
/// <see cref="IEnumerable{IMember}" />
285+
/// </returns>
286+
IEnumerable<IMember> GetMembersByEmail(string email)
287+
=>
288+
// TODO (V16): Remove this default implementation.
289+
// The following is very inefficient, but will return the correct data, so probably better than throwing a NotImplementedException
290+
// in the default implentation here, for, presumably rare, cases where a custom IMemberService implementation has been registered and
291+
// does not override this method.
292+
GetAllMembers().Where(x => x.Email.Equals(email));
293+
270294
/// <summary>
271295
/// Gets all Members for the specified MemberType alias
272296
/// </summary>

src/Umbraco.Core/Services/MemberService.cs

Lines changed: 28 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -408,16 +408,23 @@ public IEnumerable<IMember> GetAll(
408408
}
409409

410410
/// <summary>
411-
/// Get an <see cref="IMember"/> by email
411+
/// 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.
412412
/// </summary>
413413
/// <param name="email">Email to use for retrieval</param>
414414
/// <returns><see cref="IMember"/></returns>
415-
public IMember? GetByEmail(string email)
415+
public IMember? GetByEmail(string email) => GetMembersByEmail(email).FirstOrDefault();
416+
417+
/// <summary>
418+
/// Get an list of <see cref="IMember"/> for all members with the specified email.
419+
/// </summary>
420+
/// <param name="email">Email to use for retrieval</param>
421+
/// <returns><see cref="IEnumerable{IMember}"/></returns>
422+
public IEnumerable<IMember> GetMembersByEmail(string email)
416423
{
417424
using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true);
418425
scope.ReadLock(Constants.Locks.MemberTree);
419426
IQuery<IMember> query = Query<IMember>().Where(x => x.Email.Equals(email));
420-
return _memberRepository.Get(query)?.FirstOrDefault();
427+
return _memberRepository.Get(query);
421428
}
422429

423430
/// <summary>
@@ -765,6 +772,9 @@ public bool Exists(string username)
765772

766773
/// <inheritdoc />
767774
public Attempt<OperationResult?> Save(IMember member, int userId = Constants.Security.SuperUserId)
775+
=> Save(member, PublishNotificationSaveOptions.All, userId);
776+
777+
public Attempt<OperationResult?> Save(IMember member, PublishNotificationSaveOptions publishNotificationSaveOptions, int userId = Constants.Security.SuperUserId)
768778
{
769779
// trimming username and email to make sure we have no trailing space
770780
member.Username = member.Username.Trim();
@@ -773,11 +783,15 @@ public bool Exists(string username)
773783
EventMessages evtMsgs = EventMessagesFactory.Get();
774784

775785
using ICoreScope scope = ScopeProvider.CreateCoreScope();
776-
var savingNotification = new MemberSavingNotification(member, evtMsgs);
777-
if (scope.Notifications.PublishCancelable(savingNotification))
786+
MemberSavingNotification? savingNotification = null;
787+
if (publishNotificationSaveOptions.HasFlag(PublishNotificationSaveOptions.Saving))
778788
{
779-
scope.Complete();
780-
return OperationResult.Attempt.Cancel(evtMsgs);
789+
savingNotification = new MemberSavingNotification(member, evtMsgs);
790+
if (scope.Notifications.PublishCancelable(savingNotification))
791+
{
792+
scope.Complete();
793+
return OperationResult.Attempt.Cancel(evtMsgs);
794+
}
781795
}
782796

783797
if (string.IsNullOrWhiteSpace(member.Name))
@@ -789,7 +803,13 @@ public bool Exists(string username)
789803

790804
_memberRepository.Save(member);
791805

792-
scope.Notifications.Publish(new MemberSavedNotification(member, evtMsgs).WithStateFrom(savingNotification));
806+
if (publishNotificationSaveOptions.HasFlag(PublishNotificationSaveOptions.Saved))
807+
{
808+
scope.Notifications.Publish(
809+
savingNotification is null
810+
? new MemberSavedNotification(member, evtMsgs)
811+
: new MemberSavedNotification(member, evtMsgs).WithStateFrom(savingNotification));
812+
}
793813

794814
Audit(AuditType.Save, 0, member.Id);
795815

src/Umbraco.Core/Services/UserServiceExtensions.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,6 @@ public static IEnumerable<IProfile> GetProfilesById(this IUserService userServic
8787
});
8888
}
8989

90-
[Obsolete("Use IUserService.Get that takes a Guid instead. Scheduled for removal in V15.")]
90+
[Obsolete("Use IUserService.GetAsync that takes a Guid instead. Scheduled for removal in V15.")]
9191
public static IUser? GetByKey(this IUserService userService, Guid key) => userService.GetAsync(key).GetAwaiter().GetResult();
9292
}

0 commit comments

Comments
 (0)