Skip to content

Commit

Permalink
Merge branch 'v15/dev' into v15/bugfix/dont-delete-when-referenced
Browse files Browse the repository at this point in the history
  • Loading branch information
Zeegaan authored Feb 19, 2025
2 parents 1602c55 + 040d4fe commit 0c0b577
Show file tree
Hide file tree
Showing 102 changed files with 2,677 additions and 799 deletions.
41 changes: 26 additions & 15 deletions src/Umbraco.Cms.Api.Delivery/Services/RequestRedirectService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -42,31 +42,42 @@ public RequestRedirectService(
{
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;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using Asp.Versioning;
using Asp.Versioning;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Api.Common.ViewModels.Pagination;
Expand Down Expand Up @@ -37,7 +37,7 @@ public async Task<ActionResult<PagedViewModel<IReferenceResponseModel>>> Referen
int skip = 0,
int take = 20)
{
PagedModel<RelationItemModel> relationItems = await _trackedReferencesService.GetPagedRelationsForItemAsync(id, skip, take, false);
PagedModel<RelationItemModel> relationItems = await _trackedReferencesService.GetPagedRelationsForItemAsync(id, skip, take, true);

var pagedViewModel = new PagedViewModel<IReferenceResponseModel>
{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using Asp.Versioning;
using Asp.Versioning;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Api.Common.ViewModels.Pagination;
Expand Down Expand Up @@ -37,7 +37,7 @@ public async Task<ActionResult<PagedViewModel<IReferenceResponseModel>>> Referen
int skip = 0,
int take = 20)
{
PagedModel<RelationItemModel> relationItems = await _trackedReferencesService.GetPagedRelationsForItemAsync(id, skip, take, false);
PagedModel<RelationItemModel> relationItems = await _trackedReferencesService.GetPagedRelationsForItemAsync(id, skip, take, true);

var pagedViewModel = new PagedViewModel<IReferenceResponseModel>
{
Expand Down
7 changes: 7 additions & 0 deletions src/Umbraco.Core/Cache/RepositoryCachePolicyOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ public class RepositoryCachePolicyOptions
public RepositoryCachePolicyOptions(Func<int> performCount)
{
PerformCount = performCount;
CacheNullValues = false;
GetAllCacheValidateCount = true;
GetAllCacheAllowZeroCount = false;
}
Expand All @@ -21,6 +22,7 @@ public RepositoryCachePolicyOptions(Func<int> performCount)
public RepositoryCachePolicyOptions()
{
PerformCount = null;
CacheNullValues = false;
GetAllCacheValidateCount = false;
GetAllCacheAllowZeroCount = false;
}
Expand All @@ -30,6 +32,11 @@ public 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
Expand Down
2 changes: 1 addition & 1 deletion src/Umbraco.Core/Composing/TypeFinder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
13 changes: 13 additions & 0 deletions src/Umbraco.Core/Configuration/Models/ModelsBuilderSettings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down Expand Up @@ -78,4 +79,16 @@ public bool FlagOutOfDateModels
/// <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;
}
11 changes: 10 additions & 1 deletion src/Umbraco.Core/Configuration/Models/SecuritySettings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ 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-._@+\\";
Expand Down Expand Up @@ -64,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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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 }))
{
Expand Down
28 changes: 28 additions & 0 deletions src/Umbraco.Core/Models/PublishNotificationSaveOptions.cs
Original file line number Diff line number Diff line change
@@ -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,
}
2 changes: 1 addition & 1 deletion src/Umbraco.Core/PublishedCache/ITagQuery.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
2 changes: 0 additions & 2 deletions src/Umbraco.Core/Routing/NewDefaultUrlProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -89,8 +89,6 @@ public virtual IEnumerable<UrlInfo> GetOtherUrls(int id, Uri current)
yield break;
}



// look for domains, walking up the tree
IPublishedContent? n = node;
IEnumerable<DomainAndUri>? domainUris =
Expand Down
8 changes: 7 additions & 1 deletion src/Umbraco.Core/Routing/PublishedUrlInfoProvider.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.PublishedContent;
using Umbraco.Cms.Core.Services;
Expand Down Expand Up @@ -68,6 +68,12 @@ public async Task<ISet<UrlInfo>> GetAllAsync(IContent content)
urlInfos.Add(UrlInfo.Url(url, culture));
}

// If the content is trashed, we can't get the other URLs, as we have no parent structure to navigate through.
if (content.Trashed)
{
return urlInfos;
}

// Then get "other" urls - I.E. Not what you'd get with GetUrl(), this includes all the urls registered using domains.
// for these 'other' URLs, we don't check whether they are routable, collide, anything - we just report them.
foreach (UrlInfo otherUrl in _publishedUrlProvider.GetOtherUrls(content.Id).OrderBy(x => x.Text).ThenBy(x => x.Culture))
Expand Down
24 changes: 24 additions & 0 deletions src/Umbraco.Core/Services/IMemberService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,15 @@ IMember CreateMemberWithIdentity(string username, string email, string name, str
/// </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>
Expand Down Expand Up @@ -268,6 +277,21 @@ IMember CreateMemberWithIdentity(string username, string email, string name, str
/// </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>
Expand Down
36 changes: 28 additions & 8 deletions src/Umbraco.Core/Services/MemberService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -408,16 +408,23 @@ public IEnumerable<IMember> GetAll(
}

/// <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>
Expand Down Expand Up @@ -765,6 +772,9 @@ public bool Exists(string username)

/// <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();
Expand All @@ -773,11 +783,15 @@ public bool Exists(string username)
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))
Expand All @@ -789,7 +803,13 @@ public bool Exists(string username)

_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, userId, member.Id);

Expand Down
2 changes: 1 addition & 1 deletion src/Umbraco.Core/Services/UserServiceExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,6 @@ public static IEnumerable<IProfile> GetProfilesById(this IUserService userServic
});
}

[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();
}
Loading

0 comments on commit 0c0b577

Please sign in to comment.