Skip to content

Commit cfb0fc2

Browse files
committed
Merge branch 'v13/dev' into v13/contrib
2 parents 2422a02 + 6620aca commit cfb0fc2

File tree

9 files changed

+142
-32
lines changed

9 files changed

+142
-32
lines changed

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/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
{

src/Umbraco.Infrastructure/Cache/DefaultRepositoryCachePolicy.cs

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ public class DefaultRepositoryCachePolicy<TEntity, TId> : RepositoryCachePolicyB
2424
private static readonly TEntity[] _emptyEntities = new TEntity[0]; // const
2525
private readonly RepositoryCachePolicyOptions _options;
2626

27+
private const string NullRepresentationInCache = "*NULL*";
28+
2729
public DefaultRepositoryCachePolicy(IAppPolicyCache cache, IScopeAccessor scopeAccessor, RepositoryCachePolicyOptions options)
2830
: base(cache, scopeAccessor) =>
2931
_options = options ?? throw new ArgumentNullException(nameof(options));
@@ -116,6 +118,7 @@ public override void Delete(TEntity entity, Action<TEntity> persistDeleted)
116118
{
117119
// whatever happens, clear the cache
118120
var cacheKey = GetEntityCacheKey(entity.Id);
121+
119122
Cache.Clear(cacheKey);
120123

121124
// if there's a GetAllCacheAllowZeroCount cache, ensure it is cleared
@@ -127,20 +130,36 @@ public override void Delete(TEntity entity, Action<TEntity> persistDeleted)
127130
public override TEntity? Get(TId? id, Func<TId?, TEntity?> performGet, Func<TId[]?, IEnumerable<TEntity>?> performGetAll)
128131
{
129132
var cacheKey = GetEntityCacheKey(id);
133+
130134
TEntity? fromCache = Cache.GetCacheItem<TEntity>(cacheKey);
131135

132-
// if found in cache then return else fetch and cache
133-
if (fromCache != null)
136+
// If found in cache then return immediately.
137+
if (fromCache is not null)
134138
{
135139
return fromCache;
136140
}
137141

142+
// Because TEntity can never be a string, we will never be in a position where the proxy value collides withs a real value.
143+
// 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.
144+
// If we've cached a "null" value, return null.
145+
if (_options.CacheNullValues && Cache.GetCacheItem<string>(cacheKey) == NullRepresentationInCache)
146+
{
147+
return null;
148+
}
149+
150+
// Otherwise go to the database to retrieve.
138151
TEntity? entity = performGet(id);
139152

140153
if (entity != null && entity.HasIdentity)
141154
{
155+
// If we've found an identified entity, cache it for subsequent retrieval.
142156
InsertEntity(cacheKey, entity);
143157
}
158+
else if (entity is null && _options.CacheNullValues)
159+
{
160+
// If we've not found an entity, and we're caching null values, cache a "null" value.
161+
InsertNull(cacheKey);
162+
}
144163

145164
return entity;
146165
}
@@ -248,6 +267,15 @@ protected string GetEntityCacheKey(TId? id)
248267
protected virtual void InsertEntity(string cacheKey, TEntity entity)
249268
=> Cache.Insert(cacheKey, () => entity, TimeSpan.FromMinutes(5), true);
250269

270+
protected virtual void InsertNull(string cacheKey)
271+
{
272+
// We can't actually cache a null value, as in doing so wouldn't be able to distinguish between
273+
// a value that does exist but isn't yet cached, or a value that has been explicitly cached with a null value.
274+
// Both would return null when we retrieve from the cache and we couldn't distinguish between the two.
275+
// So we cache a special value that represents null, and then we can check for that value when we retrieve from the cache.
276+
Cache.Insert(cacheKey, () => NullRepresentationInCache, TimeSpan.FromMinutes(5), true);
277+
}
278+
251279
protected virtual void InsertEntities(TId[]? ids, TEntity[]? entities)
252280
{
253281
if (ids?.Length == 0 && entities?.Length == 0 && _options.GetAllCacheAllowZeroCount)

src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DictionaryRepository.cs

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -102,11 +102,10 @@ protected override IRepositoryCachePolicy<IDictionaryItem, int> CreateCachePolic
102102
var options = new RepositoryCachePolicyOptions
103103
{
104104
// allow zero to be cached
105-
GetAllCacheAllowZeroCount = true,
105+
GetAllCacheAllowZeroCount = true
106106
};
107107

108-
return new SingleItemsOnlyRepositoryCachePolicy<IDictionaryItem, int>(GlobalIsolatedCache, ScopeAccessor,
109-
options);
108+
return new SingleItemsOnlyRepositoryCachePolicy<IDictionaryItem, int>(GlobalIsolatedCache, ScopeAccessor, options);
110109
}
111110

112111
protected IDictionaryItem ConvertFromDto(DictionaryDto dto)
@@ -190,11 +189,10 @@ protected override IRepositoryCachePolicy<IDictionaryItem, Guid> CreateCachePoli
190189
var options = new RepositoryCachePolicyOptions
191190
{
192191
// allow zero to be cached
193-
GetAllCacheAllowZeroCount = true,
192+
GetAllCacheAllowZeroCount = true
194193
};
195194

196-
return new SingleItemsOnlyRepositoryCachePolicy<IDictionaryItem, Guid>(GlobalIsolatedCache, ScopeAccessor,
197-
options);
195+
return new SingleItemsOnlyRepositoryCachePolicy<IDictionaryItem, Guid>(GlobalIsolatedCache, ScopeAccessor, options);
198196
}
199197
}
200198

@@ -228,12 +226,13 @@ protected override IRepositoryCachePolicy<IDictionaryItem, string> CreateCachePo
228226
{
229227
var options = new RepositoryCachePolicyOptions
230228
{
229+
// allow null to be cached
230+
CacheNullValues = true,
231231
// allow zero to be cached
232-
GetAllCacheAllowZeroCount = true,
232+
GetAllCacheAllowZeroCount = true
233233
};
234234

235-
return new SingleItemsOnlyRepositoryCachePolicy<IDictionaryItem, string>(GlobalIsolatedCache, ScopeAccessor,
236-
options);
235+
return new SingleItemsOnlyRepositoryCachePolicy<IDictionaryItem, string>(GlobalIsolatedCache, ScopeAccessor, options);
237236
}
238237
}
239238

src/Umbraco.Infrastructure/PropertyEditors/RichTextEditorPastedImages.cs

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,14 @@
88
using Microsoft.Extensions.DependencyInjection;
99
using Microsoft.Extensions.Logging;
1010
using Microsoft.Extensions.Options;
11+
using Umbraco.Cms.Core.Cache;
1112
using Umbraco.Cms.Core.Configuration.Models;
1213
using Umbraco.Cms.Core.Exceptions;
1314
using Umbraco.Cms.Core.Hosting;
1415
using Umbraco.Cms.Core.IO;
1516
using Umbraco.Cms.Core.Media;
1617
using Umbraco.Cms.Core.Models;
18+
using Umbraco.Cms.Core.Models.Membership;
1719
using Umbraco.Cms.Core.Models.PublishedContent;
1820
using Umbraco.Cms.Core.Routing;
1921
using Umbraco.Cms.Core.Services;
@@ -38,6 +40,9 @@ public sealed class RichTextEditorPastedImages
3840
private readonly IUmbracoContextAccessor _umbracoContextAccessor;
3941
private readonly string _tempFolderAbsolutePath;
4042
private readonly IImageUrlGenerator _imageUrlGenerator;
43+
private readonly IEntityService _entityService;
44+
private readonly IUserService _userService;
45+
private readonly AppCaches _appCaches;
4146
private readonly ContentSettings _contentSettings;
4247
private readonly Dictionary<string, GuidUdi> _uploadedImages = new();
4348

@@ -67,6 +72,7 @@ public RichTextEditorPastedImages(
6772
{
6873
}
6974

75+
[Obsolete("Use the non-obsolete constructor. Scheduled for removal in v14")]
7076
public RichTextEditorPastedImages(
7177
IUmbracoContextAccessor umbracoContextAccessor,
7278
ILogger<RichTextEditorPastedImages> logger,
@@ -79,6 +85,39 @@ public RichTextEditorPastedImages(
7985
IPublishedUrlProvider publishedUrlProvider,
8086
IImageUrlGenerator imageUrlGenerator,
8187
IOptions<ContentSettings> contentSettings)
88+
: this(
89+
umbracoContextAccessor,
90+
logger,
91+
hostingEnvironment,
92+
mediaService,
93+
contentTypeBaseServiceProvider,
94+
mediaFileManager,
95+
mediaUrlGenerators,
96+
shortStringHelper,
97+
publishedUrlProvider,
98+
imageUrlGenerator,
99+
StaticServiceProvider.Instance.GetRequiredService<IEntityService>(),
100+
StaticServiceProvider.Instance.GetRequiredService<IUserService>(),
101+
StaticServiceProvider.Instance.GetRequiredService<AppCaches>(),
102+
contentSettings)
103+
{
104+
}
105+
106+
public RichTextEditorPastedImages(
107+
IUmbracoContextAccessor umbracoContextAccessor,
108+
ILogger<RichTextEditorPastedImages> logger,
109+
IHostingEnvironment hostingEnvironment,
110+
IMediaService mediaService,
111+
IContentTypeBaseServiceProvider contentTypeBaseServiceProvider,
112+
MediaFileManager mediaFileManager,
113+
MediaUrlGeneratorCollection mediaUrlGenerators,
114+
IShortStringHelper shortStringHelper,
115+
IPublishedUrlProvider publishedUrlProvider,
116+
IImageUrlGenerator imageUrlGenerator,
117+
IEntityService entityService,
118+
IUserService userService,
119+
AppCaches appCaches,
120+
IOptions<ContentSettings> contentSettings)
82121
{
83122
_umbracoContextAccessor =
84123
umbracoContextAccessor ?? throw new ArgumentNullException(nameof(umbracoContextAccessor));
@@ -92,6 +131,9 @@ public RichTextEditorPastedImages(
92131
_shortStringHelper = shortStringHelper;
93132
_publishedUrlProvider = publishedUrlProvider;
94133
_imageUrlGenerator = imageUrlGenerator;
134+
_entityService = entityService;
135+
_userService = userService;
136+
_appCaches = appCaches;
95137
_contentSettings = contentSettings.Value;
96138

97139
_tempFolderAbsolutePath = _hostingEnvironment.MapPathContentRoot(Constants.SystemDirectories.TempImageUploads);
@@ -270,7 +312,7 @@ private void PersistMediaItem(Guid mediaParentFolder, int userId, HtmlNode img,
270312
: Constants.Conventions.MediaTypes.Image;
271313

272314
IMedia mediaFile = mediaParentFolder == Guid.Empty
273-
? _mediaService.CreateMedia(mediaItemName, Constants.System.Root, mediaType, userId)
315+
? _mediaService.CreateMedia(mediaItemName, GetDefaultMediaRoot(userId), mediaType, userId)
274316
: _mediaService.CreateMedia(mediaItemName, mediaParentFolder, mediaType, userId);
275317

276318
var fileInfo = new FileInfo(absoluteTempImagePath);
@@ -354,4 +396,11 @@ private void PersistMediaItem(Guid mediaParentFolder, int userId, HtmlNode img,
354396
}
355397

356398
private bool IsValidPath(string imagePath) => imagePath.StartsWith(_tempFolderAbsolutePath);
399+
400+
private int GetDefaultMediaRoot(int userId)
401+
{
402+
IUser user = _userService.GetUserById(userId) ?? throw new ArgumentException("User could not be found");
403+
var userStartNodes = user.CalculateMediaStartNodeIds(_entityService, _appCaches);
404+
return userStartNodes?.FirstOrDefault() ?? Constants.System.Root;
405+
}
357406
}

src/Umbraco.Web.BackOffice/Trees/ContentTreeController.cs

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -317,13 +317,7 @@ protected MenuItemCollection GetAllNodeMenuItems(IUmbracoEntity item)
317317

318318
if (_emailSender.CanSendRequiredEmail())
319319
{
320-
menu.Items.Add(new MenuItem("notify", LocalizedTextService)
321-
{
322-
Icon = "icon-megaphone",
323-
SeparatorBefore = true,
324-
OpensDialog = true,
325-
UseLegacyIcon = false
326-
});
320+
AddActionNode<ActionNotify>(item, menu, hasSeparator: true, opensDialog: true, useLegacyIcon: false);
327321
}
328322

329323
if ((item is DocumentEntitySlim documentEntity && documentEntity.IsContainer) == false)

src/Umbraco.Web.Common/Extensions/HttpContextExtensions.cs

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,9 +62,16 @@ public static async Task<AuthenticateResult> AuthenticateBackOfficeAsync(this Ht
6262
// Update the HttpContext's user with the authenticated user's principal to ensure
6363
// that subsequent requests within the same context will recognize the user
6464
// as authenticated.
65-
if (result.Succeeded)
65+
if (result is { Succeeded: true, Principal.Identity: not null })
6666
{
67-
httpContext.User = result.Principal;
67+
// We need to get existing identities that are not the backoffice kind and flow them to the new identity
68+
// Otherwise we can't log in as both a member and a backoffice user
69+
// For instance if you've enabled basic auth.
70+
ClaimsPrincipal? authenticatedPrincipal = result.Principal;
71+
IEnumerable<ClaimsIdentity> existingIdentities = httpContext.User.Identities.Where(x => x.IsAuthenticated && x.AuthenticationType != authenticatedPrincipal.Identity.AuthenticationType);
72+
authenticatedPrincipal.AddIdentities(existingIdentities);
73+
74+
httpContext.User = authenticatedPrincipal;
6875
}
6976

7077
return result;
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
using System.Security.Claims;
2+
using Microsoft.AspNetCore.Identity;
3+
4+
namespace Umbraco.Extensions;
5+
6+
public static class MemberClaimsPrincipalExtensions
7+
{
8+
/// <summary>
9+
/// Tries to get specifically the member identity from the ClaimsPrincipal
10+
/// </summary>
11+
/// <remarks>
12+
/// The identity returned is the one with default authentication type.
13+
/// </remarks>
14+
/// <param name="principal">The principal to find the identity in.</param>
15+
/// <returns>The default authenticated authentication type identity.</returns>
16+
public static ClaimsIdentity? GetMemberIdentity(this ClaimsPrincipal principal)
17+
=> principal.Identities.FirstOrDefault(x => x.AuthenticationType == IdentityConstants.ApplicationScheme);
18+
}

src/Umbraco.Web.Common/Security/MemberManager.cs

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using System.Globalization;
2+
using System.Security.Claims;
23
using Microsoft.AspNetCore.Http;
34
using Microsoft.AspNetCore.Identity;
45
using Microsoft.Extensions.Logging;
@@ -124,8 +125,11 @@ public virtual async Task<bool> IsMemberAuthorizedAsync(
124125
/// <inheritdoc />
125126
public virtual bool IsLoggedIn()
126127
{
127-
HttpContext? httpContext = _httpContextAccessor.HttpContext;
128-
return httpContext?.User.Identity?.IsAuthenticated ?? false;
128+
// We have to try and specifically find the member identity, it's entirely possible for there to be both backoffice and member.
129+
ClaimsIdentity? memberIdentity = _httpContextAccessor.HttpContext?.User.GetMemberIdentity();
130+
131+
return memberIdentity is not null &&
132+
memberIdentity.IsAuthenticated;
129133
}
130134

131135
/// <inheritdoc />
@@ -181,23 +185,27 @@ public virtual Task<IReadOnlyDictionary<string, bool>> IsProtectedAsync(IEnumera
181185
/// <inheritdoc />
182186
public virtual async Task<MemberIdentityUser?> GetCurrentMemberAsync()
183187
{
184-
if (_currentMember == null)
188+
if (_currentMember is not null)
185189
{
186-
if (!IsLoggedIn())
187-
{
188-
return null;
189-
}
190+
return _currentMember;
191+
}
190192

191-
_currentMember = await GetUserAsync(_httpContextAccessor.HttpContext?.User!);
193+
if (IsLoggedIn() is false)
194+
{
195+
return null;
192196
}
193197

198+
// Create a principal the represents the member security context.
199+
var memberPrincipal = new ClaimsPrincipal(_httpContextAccessor.HttpContext?.User.GetMemberIdentity()!);
200+
_currentMember = await GetUserAsync(memberPrincipal);
201+
194202
return _currentMember;
195203
}
196204

197205
public virtual IPublishedContent? AsPublishedMember(MemberIdentityUser user) => _store.GetPublishedMember(user);
198206

199207
/// <summary>
200-
/// This will check if the member has access to this path
208+
/// This will check if the member has access to this path.
201209
/// </summary>
202210
/// <param name="path"></param>
203211
/// <returns></returns>

0 commit comments

Comments
 (0)