Files
Umbraco-CMS/src/Umbraco.Core/Models/UserExtensions.cs
Mole c9ebaadf23 Netcore: File systems rework (#10181)
* Allow IMediaFileSystem to be replace in the DI, or registered with inner filesystem

* Remove GetFileSystem from Filesystems

It was only used by tests.

* Make MediaFileSystem inherit from PhysicalFileSystem directly

* Remove FileSystemWrapper

* Remove inner filesystem from MediaFileSystem

* Add MediaFileManager and bare minimum to make it testable

* Remove MediaFileSystem

* Fix unit tests using MediaFileManager

* Remove IFileSystem and rely only on FileSystem

* Hide dangerous methods in FileSystems and do some cleaning

* Apply stylecop warnings to MediaFileManager

* Add FilesystemsCreator to Tests.Common

This allows you to create an instance if FileSystems with your own specified IFileSystem for testing purposes outside our own test suite.

* Allow the stylesheet filesystem to be replaced.

* Fix tests

* Don't save stylesheetWrapper in a temporary var

* refactor(FileSystems): change how stylesheet filesystem is registered

* fix(FileSystems): unable to overwrite media filesystem

SetMediaFileSystem added the MediaManager as a Singleton instead of
replacing the existing instance.

* fix(FileSystems): calling AddFileSystems replaces MediaManager

When calling AddFileSystems after SetMediaFileSystem the MediaManager
gets replaced by the default PhysicalFileSystem, so instead of calling
SetMediaFileSystem in AddFileSystems we now call TrySetMediaFileSystem
instead. This method will not replace any existing instance of the
MediaManager if there's already a MediaManager registered.

* Use SetMediaFileSystem instead of TrySet, and rename AddFilesystems to ConfigureFileSystems

Also don't call AddFileSystems again in ConfigureFilesystems

* Don't wrap CSS filesystem twice

* Add CreateShadowWrapperInternal to avoid casting

* Throw UnauthorizedAccessException isntead of InvalidOperationException

* Remove ResetShadowId

Co-authored-by: Rasmus John Pedersen <mail@rjp.dk>
2021-04-27 09:52:17 +02:00

285 lines
13 KiB
C#

using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Security.Cryptography;
using Umbraco.Cms.Core.Cache;
using Umbraco.Cms.Core.IO;
using Umbraco.Cms.Core.Media;
using Umbraco.Cms.Core.Models.Entities;
using Umbraco.Cms.Core.Models.Membership;
using Umbraco.Cms.Core.Security;
using Umbraco.Cms.Core.Services;
using Umbraco.Extensions;
namespace Umbraco.Cms.Core.Models
{
public static class UserExtensions
{
/// <summary>
/// Tries to lookup the user's Gravatar to see if the endpoint can be reached, if so it returns the valid URL
/// </summary>
/// <param name="user"></param>
/// <param name="cache"></param>
/// <param name="mediaFileManager"></param>
/// <returns>
/// A list of 5 different sized avatar URLs
/// </returns>
public static string[] GetUserAvatarUrls(this IUser user, IAppCache cache, MediaFileManager mediaFileManager, IImageUrlGenerator imageUrlGenerator)
{
// If FIPS is required, never check the Gravatar service as it only supports MD5 hashing.
// Unfortunately, if the FIPS setting is enabled on Windows, using MD5 will throw an exception
// and the website will not run.
// Also, check if the user has explicitly removed all avatars including a Gravatar, this will be possible and the value will be "none"
if (user.Avatar == "none" || CryptoConfig.AllowOnlyFipsAlgorithms)
{
return new string[0];
}
if (user.Avatar.IsNullOrWhiteSpace())
{
var gravatarHash = user.Email.GenerateHash<MD5>();
var gravatarUrl = "https://www.gravatar.com/avatar/" + gravatarHash + "?d=404";
//try Gravatar
var gravatarAccess = cache.GetCacheItem<bool>("UserAvatar" + user.Id, () =>
{
// Test if we can reach this URL, will fail when there's network or firewall errors
var request = (HttpWebRequest)WebRequest.Create(gravatarUrl);
// Require response within 10 seconds
request.Timeout = 10000;
try
{
using ((HttpWebResponse)request.GetResponse()) { }
}
catch (Exception)
{
// There was an HTTP or other error, return an null instead
return false;
}
return true;
});
if (gravatarAccess)
{
return new[]
{
gravatarUrl + "&s=30",
gravatarUrl + "&s=60",
gravatarUrl + "&s=90",
gravatarUrl + "&s=150",
gravatarUrl + "&s=300"
};
}
return new string[0];
}
//use the custom avatar
var avatarUrl = mediaFileManager.FileSystem.GetUrl(user.Avatar);
return new[]
{
imageUrlGenerator.GetImageUrl(new ImageUrlGenerationOptions(avatarUrl) { ImageCropMode = ImageCropMode.Crop, Width = 30, Height = 30 }),
imageUrlGenerator.GetImageUrl(new ImageUrlGenerationOptions(avatarUrl) { ImageCropMode = ImageCropMode.Crop, Width = 60, Height = 60 }),
imageUrlGenerator.GetImageUrl(new ImageUrlGenerationOptions(avatarUrl) { ImageCropMode = ImageCropMode.Crop, Width = 90, Height = 90 }),
imageUrlGenerator.GetImageUrl(new ImageUrlGenerationOptions(avatarUrl) { ImageCropMode = ImageCropMode.Crop, Width = 150, Height = 150 }),
imageUrlGenerator.GetImageUrl(new ImageUrlGenerationOptions(avatarUrl) { ImageCropMode = ImageCropMode.Crop, Width = 300, Height = 300 })
};
}
internal static bool HasContentRootAccess(this IUser user, IEntityService entityService, AppCaches appCaches)
{
return ContentPermissions.HasPathAccess(Constants.System.RootString, user.CalculateContentStartNodeIds(entityService, appCaches), Constants.System.RecycleBinContent);
}
internal static bool HasContentBinAccess(this IUser user, IEntityService entityService, AppCaches appCaches)
{
return ContentPermissions.HasPathAccess(Constants.System.RecycleBinContentString, user.CalculateContentStartNodeIds(entityService, appCaches), Constants.System.RecycleBinContent);
}
internal static bool HasMediaRootAccess(this IUser user, IEntityService entityService, AppCaches appCaches)
{
return ContentPermissions.HasPathAccess(Constants.System.RootString, user.CalculateMediaStartNodeIds(entityService, appCaches), Constants.System.RecycleBinMedia);
}
internal static bool HasMediaBinAccess(this IUser user, IEntityService entityService, AppCaches appCaches)
{
return ContentPermissions.HasPathAccess(Constants.System.RecycleBinMediaString, user.CalculateMediaStartNodeIds(entityService, appCaches), Constants.System.RecycleBinMedia);
}
public static bool HasPathAccess(this IUser user, IContent content, IEntityService entityService, AppCaches appCaches)
{
if (content == null) throw new ArgumentNullException(nameof(content));
return ContentPermissions.HasPathAccess(content.Path, user.CalculateContentStartNodeIds(entityService, appCaches), Constants.System.RecycleBinContent);
}
public static bool HasPathAccess(this IUser user, IMedia media, IEntityService entityService, AppCaches appCaches)
{
if (media == null) throw new ArgumentNullException(nameof(media));
return ContentPermissions.HasPathAccess(media.Path, user.CalculateMediaStartNodeIds(entityService, appCaches), Constants.System.RecycleBinMedia);
}
public static bool HasContentPathAccess(this IUser user, IUmbracoEntity entity, IEntityService entityService, AppCaches appCaches)
{
if (entity == null) throw new ArgumentNullException(nameof(entity));
return ContentPermissions.HasPathAccess(entity.Path, user.CalculateContentStartNodeIds(entityService, appCaches), Constants.System.RecycleBinContent);
}
public static bool HasMediaPathAccess(this IUser user, IUmbracoEntity entity, IEntityService entityService, AppCaches appCaches)
{
if (entity == null) throw new ArgumentNullException(nameof(entity));
return ContentPermissions.HasPathAccess(entity.Path, user.CalculateMediaStartNodeIds(entityService, appCaches), Constants.System.RecycleBinMedia);
}
/// <summary>
/// Determines whether this user has access to view sensitive data
/// </summary>
/// <param name="user"></param>
public static bool HasAccessToSensitiveData(this IUser user)
{
if (user == null) throw new ArgumentNullException("user");
return user.Groups != null && user.Groups.Any(x => x.Alias == Constants.Security.SensitiveDataGroupAlias);
}
/// <summary>
/// Calculate start nodes, combining groups' and user's, and excluding what's in the bin
/// </summary>
public static int[] CalculateContentStartNodeIds(this IUser user, IEntityService entityService, AppCaches appCaches)
{
var cacheKey = CacheKeys.UserAllContentStartNodesPrefix + user.Id;
var runtimeCache = appCaches.IsolatedCaches.GetOrCreate<IUser>();
var result = runtimeCache.GetCacheItem(cacheKey, () =>
{
var gsn = user.Groups.Where(x => x.StartContentId.HasValue).Select(x => x.StartContentId.Value).Distinct().ToArray();
var usn = user.StartContentIds;
var vals = CombineStartNodes(UmbracoObjectTypes.Document, gsn, usn, entityService);
return vals;
}, TimeSpan.FromMinutes(2), true);
return result;
}
/// <summary>
/// Calculate start nodes, combining groups' and user's, and excluding what's in the bin
/// </summary>
/// <param name="user"></param>
/// <param name="entityService"></param>
/// <param name="runtimeCache"></param>
/// <returns></returns>
public static int[] CalculateMediaStartNodeIds(this IUser user, IEntityService entityService, AppCaches appCaches)
{
var cacheKey = CacheKeys.UserAllMediaStartNodesPrefix + user.Id;
var runtimeCache = appCaches.IsolatedCaches.GetOrCreate<IUser>();
var result = runtimeCache.GetCacheItem(cacheKey, () =>
{
var gsn = user.Groups.Where(x => x.StartMediaId.HasValue).Select(x => x.StartMediaId.Value).Distinct().ToArray();
var usn = user.StartMediaIds;
var vals = CombineStartNodes(UmbracoObjectTypes.Media, gsn, usn, entityService);
return vals;
}, TimeSpan.FromMinutes(2), true);
return result;
}
public static string[] GetMediaStartNodePaths(this IUser user, IEntityService entityService, AppCaches appCaches)
{
var cacheKey = CacheKeys.UserMediaStartNodePathsPrefix + user.Id;
var runtimeCache = appCaches.IsolatedCaches.GetOrCreate<IUser>();
var result = runtimeCache.GetCacheItem(cacheKey, () =>
{
var startNodeIds = user.CalculateMediaStartNodeIds(entityService, appCaches);
var vals = entityService.GetAllPaths(UmbracoObjectTypes.Media, startNodeIds).Select(x => x.Path).ToArray();
return vals;
}, TimeSpan.FromMinutes(2), true);
return result;
}
public static string[] GetContentStartNodePaths(this IUser user, IEntityService entityService, AppCaches appCaches)
{
var cacheKey = CacheKeys.UserContentStartNodePathsPrefix + user.Id;
var runtimeCache = appCaches.IsolatedCaches.GetOrCreate<IUser>();
var result = runtimeCache.GetCacheItem(cacheKey, () =>
{
var startNodeIds = user.CalculateContentStartNodeIds(entityService, appCaches);
var vals = entityService.GetAllPaths(UmbracoObjectTypes.Document, startNodeIds).Select(x => x.Path).ToArray();
return vals;
}, TimeSpan.FromMinutes(2), true);
return result;
}
private static bool StartsWithPath(string test, string path)
{
return test.StartsWith(path) && test.Length > path.Length && test[path.Length] == ',';
}
private static string GetBinPath(UmbracoObjectTypes objectType)
{
var binPath = Constants.System.Root + ",";
switch (objectType)
{
case UmbracoObjectTypes.Document:
binPath += Constants.System.RecycleBinContent;
break;
case UmbracoObjectTypes.Media:
binPath += Constants.System.RecycleBinMedia;
break;
default:
throw new ArgumentOutOfRangeException(nameof(objectType));
}
return binPath;
}
internal static int[] CombineStartNodes(UmbracoObjectTypes objectType, int[] groupSn, int[] userSn, IEntityService entityService)
{
// assume groupSn and userSn each don't contain duplicates
var asn = groupSn.Concat(userSn).Distinct().ToArray();
var paths = asn.Length > 0
? entityService.GetAllPaths(objectType, asn).ToDictionary(x => x.Id, x => x.Path)
: new Dictionary<int, string>();
paths[Constants.System.Root] = Constants.System.RootString; // entityService does not get that one
var binPath = GetBinPath(objectType);
var lsn = new List<int>();
foreach (var sn in groupSn)
{
if (paths.TryGetValue(sn, out var snp) == false) continue; // ignore rogue node (no path)
if (StartsWithPath(snp, binPath)) continue; // ignore bin
if (lsn.Any(x => StartsWithPath(snp, paths[x]))) continue; // skip if something above this sn
lsn.RemoveAll(x => StartsWithPath(paths[x], snp)); // remove anything below this sn
lsn.Add(sn);
}
var usn = new List<int>();
foreach (var sn in userSn)
{
if (paths.TryGetValue(sn, out var snp) == false) continue; // ignore rogue node (no path)
if (StartsWithPath(snp, binPath)) continue; // ignore bin
if (usn.Any(x => StartsWithPath(paths[x], snp))) continue; // skip if something below this sn
usn.RemoveAll(x => StartsWithPath(snp, paths[x])); // remove anything above this sn
usn.Add(sn);
}
foreach (var sn in usn)
{
var snp = paths[sn]; // has to be here now
lsn.RemoveAll(x => StartsWithPath(snp, paths[x]) || StartsWithPath(paths[x], snp)); // remove anything above or below this sn
lsn.Add(sn);
}
return lsn.ToArray();
}
}
}