Moved files and introduced IMarkdownToHtmlConverter to avoid packages to handle markdown in core
Signed-off-by: Bjarke Berg <mail@bergmania.dk>
This commit is contained in:
@@ -1,29 +0,0 @@
|
||||
using System;
|
||||
using NCrontab;
|
||||
using Umbraco.Core.Configuration;
|
||||
using Umbraco.Core.Configuration.Models;
|
||||
|
||||
namespace Umbraco.Infrastructure.Configuration.Extensions
|
||||
{
|
||||
public static class HealthCheckSettingsExtensions
|
||||
{
|
||||
public static TimeSpan GetNotificationDelay(this HealthChecksSettings settings, ICronTabParser cronTabParser, DateTime now, TimeSpan defaultDelay)
|
||||
{
|
||||
// If first run time not set, start with just small delay after application start.
|
||||
var firstRunTime = settings.Notification.FirstRunTime;
|
||||
if (string.IsNullOrEmpty(firstRunTime))
|
||||
{
|
||||
return defaultDelay;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Otherwise start at scheduled time according to cron expression, unless within the default delay period.
|
||||
var firstRunOccurance = cronTabParser.GetNextOccurrence(firstRunTime, now);
|
||||
var delay = firstRunOccurance - now;
|
||||
return delay < defaultDelay
|
||||
? defaultDelay
|
||||
: delay;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
using System;
|
||||
|
||||
namespace Umbraco.Core.Diagnostics
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides a collection of methods for allocating unmanaged memory, copying unmanaged memory blocks, and converting managed to unmanaged types, as well as other miscellaneous methods used when interacting with unmanaged code.
|
||||
/// </summary>
|
||||
public interface IMarchal
|
||||
{
|
||||
IntPtr GetExceptionPointers();
|
||||
}
|
||||
}
|
||||
@@ -1,137 +0,0 @@
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Runtime.InteropServices;
|
||||
using Umbraco.Core.Composing;
|
||||
using Umbraco.Core.Hosting;
|
||||
using Umbraco.Core.IO;
|
||||
|
||||
namespace Umbraco.Core.Diagnostics
|
||||
{
|
||||
// taken from https://blogs.msdn.microsoft.com/dondu/2010/10/24/writing-minidumps-in-c/
|
||||
// and https://blogs.msdn.microsoft.com/dondu/2010/10/31/writing-minidumps-from-exceptions-in-c/
|
||||
// which itself got it from http://blog.kalmbach-software.de/2008/12/13/writing-minidumps-in-c/
|
||||
|
||||
internal static class MiniDump
|
||||
{
|
||||
private static readonly object LockO = new object();
|
||||
|
||||
[Flags]
|
||||
public enum Option : uint
|
||||
{
|
||||
// From dbghelp.h:
|
||||
Normal = 0x00000000,
|
||||
WithDataSegs = 0x00000001,
|
||||
WithFullMemory = 0x00000002,
|
||||
WithHandleData = 0x00000004,
|
||||
FilterMemory = 0x00000008,
|
||||
ScanMemory = 0x00000010,
|
||||
WithUnloadedModules = 0x00000020,
|
||||
WithIndirectlyReferencedMemory = 0x00000040,
|
||||
FilterModulePaths = 0x00000080,
|
||||
WithProcessThreadData = 0x00000100,
|
||||
WithPrivateReadWriteMemory = 0x00000200,
|
||||
WithoutOptionalData = 0x00000400,
|
||||
WithFullMemoryInfo = 0x00000800,
|
||||
WithThreadInfo = 0x00001000,
|
||||
WithCodeSegs = 0x00002000,
|
||||
WithoutAuxiliaryState = 0x00004000,
|
||||
WithFullAuxiliaryState = 0x00008000,
|
||||
WithPrivateWriteCopyMemory = 0x00010000,
|
||||
IgnoreInaccessibleMemory = 0x00020000,
|
||||
ValidTypeFlags = 0x0003ffff,
|
||||
}
|
||||
|
||||
//typedef struct _MINIDUMP_EXCEPTION_INFORMATION {
|
||||
// DWORD ThreadId;
|
||||
// PEXCEPTION_POINTERS ExceptionPointers;
|
||||
// BOOL ClientPointers;
|
||||
//} MINIDUMP_EXCEPTION_INFORMATION, *PMINIDUMP_EXCEPTION_INFORMATION;
|
||||
[StructLayout(LayoutKind.Sequential, Pack = 4)] // Pack=4 is important! So it works also for x64!
|
||||
public struct MiniDumpExceptionInformation
|
||||
{
|
||||
public uint ThreadId;
|
||||
public IntPtr ExceptionPointers;
|
||||
[MarshalAs(UnmanagedType.Bool)]
|
||||
public bool ClientPointers;
|
||||
}
|
||||
|
||||
//BOOL
|
||||
//WINAPI
|
||||
//MiniDumpWriteDump(
|
||||
// __in HANDLE hProcess,
|
||||
// __in DWORD ProcessId,
|
||||
// __in HANDLE hFile,
|
||||
// __in MINIDUMP_TYPE DumpType,
|
||||
// __in_opt PMINIDUMP_EXCEPTION_INFORMATION ExceptionParam,
|
||||
// __in_opt PMINIDUMP_USER_STREAM_INFORMATION UserStreamParam,
|
||||
// __in_opt PMINIDUMP_CALLBACK_INFORMATION CallbackParam
|
||||
// );
|
||||
|
||||
// Overload requiring MiniDumpExceptionInformation
|
||||
[DllImport("dbghelp.dll", EntryPoint = "MiniDumpWriteDump", CallingConvention = CallingConvention.StdCall, CharSet = CharSet.Unicode, ExactSpelling = true, SetLastError = true)]
|
||||
private static extern bool MiniDumpWriteDump(IntPtr hProcess, uint processId, SafeHandle hFile, uint dumpType, ref MiniDumpExceptionInformation expParam, IntPtr userStreamParam, IntPtr callbackParam);
|
||||
|
||||
// Overload supporting MiniDumpExceptionInformation == NULL
|
||||
[DllImport("dbghelp.dll", EntryPoint = "MiniDumpWriteDump", CallingConvention = CallingConvention.StdCall, CharSet = CharSet.Unicode, ExactSpelling = true, SetLastError = true)]
|
||||
private static extern bool MiniDumpWriteDump(IntPtr hProcess, uint processId, SafeHandle hFile, uint dumpType, IntPtr expParam, IntPtr userStreamParam, IntPtr callbackParam);
|
||||
|
||||
[DllImport("kernel32.dll", EntryPoint = "GetCurrentThreadId", ExactSpelling = true)]
|
||||
private static extern uint GetCurrentThreadId();
|
||||
|
||||
private static bool Write(IMarchal marchal, SafeHandle fileHandle, Option options, bool withException = false)
|
||||
{
|
||||
var currentProcess = Process.GetCurrentProcess();
|
||||
var currentProcessHandle = currentProcess.Handle;
|
||||
var currentProcessId = (uint)currentProcess.Id;
|
||||
|
||||
MiniDumpExceptionInformation exp;
|
||||
|
||||
exp.ThreadId = GetCurrentThreadId();
|
||||
exp.ClientPointers = false;
|
||||
exp.ExceptionPointers = IntPtr.Zero;
|
||||
|
||||
if (withException)
|
||||
exp.ExceptionPointers = marchal.GetExceptionPointers();
|
||||
|
||||
var bRet = exp.ExceptionPointers == IntPtr.Zero
|
||||
? MiniDumpWriteDump(currentProcessHandle, currentProcessId, fileHandle, (uint) options, IntPtr.Zero, IntPtr.Zero, IntPtr.Zero)
|
||||
: MiniDumpWriteDump(currentProcessHandle, currentProcessId, fileHandle, (uint) options, ref exp, IntPtr.Zero, IntPtr.Zero);
|
||||
|
||||
return bRet;
|
||||
}
|
||||
|
||||
public static bool Dump(IMarchal marchal, IHostingEnvironment hostingEnvironment, Option options = Option.WithFullMemory, bool withException = false)
|
||||
{
|
||||
lock (LockO)
|
||||
{
|
||||
// work around "stack trace is not available while minidump debugging",
|
||||
// by making sure a local var (that we can inspect) contains the stack trace.
|
||||
// getting the call stack before it is unwound would require a special exception
|
||||
// filter everywhere in our code = not!
|
||||
var stacktrace = withException ? Environment.StackTrace : string.Empty;
|
||||
|
||||
var filepath = Path.Combine(hostingEnvironment.ApplicationPhysicalPath, "App_Data/MiniDump");
|
||||
if (Directory.Exists(filepath) == false)
|
||||
Directory.CreateDirectory(filepath);
|
||||
|
||||
var filename = Path.Combine(filepath, $"{DateTime.UtcNow:yyyyMMddTHHmmss}.{Guid.NewGuid().ToString("N").Substring(0, 4)}.dmp");
|
||||
using (var stream = new FileStream(filename, FileMode.Create, FileAccess.ReadWrite, FileShare.Write))
|
||||
{
|
||||
return Write(marchal, stream.SafeFileHandle, options, withException);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static bool OkToDump(IHostingEnvironment hostingEnvironment)
|
||||
{
|
||||
lock (LockO)
|
||||
{
|
||||
var filepath = Path.Combine(hostingEnvironment.ApplicationPhysicalPath, "App_Data/MiniDump");
|
||||
if (Directory.Exists(filepath) == false) return true;
|
||||
var count = Directory.GetFiles(filepath, "*.dmp").Length;
|
||||
return count < 8;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,142 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using Umbraco.Core.Cache;
|
||||
using Umbraco.Core.Models;
|
||||
using Umbraco.Core.Services;
|
||||
|
||||
namespace Umbraco.Core.Dictionary
|
||||
{
|
||||
|
||||
/// <summary>
|
||||
/// A culture dictionary that uses the Umbraco ILocalizationService
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// TODO: The ICultureDictionary needs to represent the 'fast' way to do dictionary item retrieval - for front-end and back office.
|
||||
/// The ILocalizationService is the service used for interacting with this data from the database which isn't all that fast
|
||||
/// (even though there is caching involved, if there's lots of dictionary items the caching is not great)
|
||||
/// </remarks>
|
||||
public class DefaultCultureDictionary : ICultureDictionary
|
||||
{
|
||||
private readonly ILocalizationService _localizationService;
|
||||
private readonly IAppCache _requestCache;
|
||||
private readonly CultureInfo _specificCulture;
|
||||
|
||||
/// <summary>
|
||||
/// Default constructor which will use the current thread's culture
|
||||
/// </summary>
|
||||
/// <param name="localizationService"></param>
|
||||
/// <param name="requestCache"></param>
|
||||
public DefaultCultureDictionary(ILocalizationService localizationService, IAppCache requestCache)
|
||||
{
|
||||
_localizationService = localizationService ?? throw new ArgumentNullException(nameof(localizationService));
|
||||
_requestCache = requestCache ?? throw new ArgumentNullException(nameof(requestCache));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Constructor for testing to specify a static culture
|
||||
/// </summary>
|
||||
/// <param name="specificCulture"></param>
|
||||
/// <param name="localizationService"></param>
|
||||
/// <param name="requestCache"></param>
|
||||
public DefaultCultureDictionary(CultureInfo specificCulture, ILocalizationService localizationService, IAppCache requestCache)
|
||||
{
|
||||
_localizationService = localizationService ?? throw new ArgumentNullException(nameof(localizationService));
|
||||
_requestCache = requestCache ?? throw new ArgumentNullException(nameof(requestCache));
|
||||
_specificCulture = specificCulture ?? throw new ArgumentNullException(nameof(specificCulture));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the dictionary value based on the key supplied
|
||||
/// </summary>
|
||||
/// <param name="key"></param>
|
||||
/// <returns></returns>
|
||||
public string this[string key]
|
||||
{
|
||||
get
|
||||
{
|
||||
var found = _localizationService.GetDictionaryItemByKey(key);
|
||||
if (found == null)
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
var byLang = found.Translations.FirstOrDefault(x => x.Language.Equals(Language));
|
||||
if (byLang == null)
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
return byLang.Value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the current culture
|
||||
/// </summary>
|
||||
public CultureInfo Culture => _specificCulture ?? System.Threading.Thread.CurrentThread.CurrentUICulture;
|
||||
|
||||
/// <summary>
|
||||
/// Returns the child dictionary entries for a given key
|
||||
/// </summary>
|
||||
/// <param name="key"></param>
|
||||
/// <returns></returns>
|
||||
/// <remarks>
|
||||
/// NOTE: The result of this is not cached anywhere - the underlying repository does not cache
|
||||
/// the child lookups because that is done by a query lookup. This method isn't used in our codebase
|
||||
/// so I don't think this is a performance issue but if devs are using this it could be optimized here.
|
||||
/// </remarks>
|
||||
public IDictionary<string, string> GetChildren(string key)
|
||||
{
|
||||
var result = new Dictionary<string, string>();
|
||||
|
||||
var found = _localizationService.GetDictionaryItemByKey(key);
|
||||
if (found == null)
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
var children = _localizationService.GetDictionaryItemChildren(found.Key);
|
||||
if (children == null)
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
foreach (var dictionaryItem in children)
|
||||
{
|
||||
var byLang = dictionaryItem.Translations.FirstOrDefault((x => x.Language.Equals(Language)));
|
||||
if (byLang != null)
|
||||
{
|
||||
result.Add(dictionaryItem.ItemKey, byLang.Value);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private ILanguage Language
|
||||
{
|
||||
get
|
||||
{
|
||||
//ensure it's stored/retrieved from request cache
|
||||
//NOTE: This is no longer necessary since these are cached at the runtime level, but we can leave it here for now.
|
||||
return _requestCache.GetCacheItem<ILanguage>(typeof (DefaultCultureDictionary).Name + "Culture" + Culture.Name,
|
||||
() => {
|
||||
// find a language that matches the current culture or any of its parent cultures
|
||||
var culture = Culture;
|
||||
while(culture != CultureInfo.InvariantCulture)
|
||||
{
|
||||
var language = _localizationService.GetLanguageByIsoCode(culture.Name);
|
||||
if(language != null)
|
||||
{
|
||||
return language;
|
||||
}
|
||||
culture = culture.Parent;
|
||||
}
|
||||
return null;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
using Umbraco.Core.Cache;
|
||||
using Umbraco.Core.Services;
|
||||
|
||||
namespace Umbraco.Core.Dictionary
|
||||
{
|
||||
/// <summary>
|
||||
/// A culture dictionary factory used to create an Umbraco.Core.Dictionary.ICultureDictionary.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// In the future this will allow use to potentially store dictionary items elsewhere and allows for maximum flexibility.
|
||||
/// </remarks>
|
||||
internal class DefaultCultureDictionaryFactory : ICultureDictionaryFactory
|
||||
{
|
||||
private readonly ILocalizationService _localizationService;
|
||||
private readonly AppCaches _appCaches;
|
||||
|
||||
public DefaultCultureDictionaryFactory(ILocalizationService localizationService, AppCaches appCaches)
|
||||
{
|
||||
_localizationService = localizationService;
|
||||
_appCaches = appCaches;
|
||||
}
|
||||
|
||||
public ICultureDictionary CreateDictionary()
|
||||
{
|
||||
return new DefaultCultureDictionary(_localizationService, _appCaches.RequestCache);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,154 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Umbraco.Core;
|
||||
using Umbraco.Core.Models;
|
||||
using Umbraco.Core.Models.Membership;
|
||||
using Umbraco.Core.Security;
|
||||
using Umbraco.Core.Services;
|
||||
|
||||
namespace Umbraco.Web.Editors
|
||||
{
|
||||
public class UserEditorAuthorizationHelper
|
||||
{
|
||||
private readonly IContentService _contentService;
|
||||
private readonly IMediaService _mediaService;
|
||||
private readonly IEntityService _entityService;
|
||||
|
||||
public UserEditorAuthorizationHelper(IContentService contentService, IMediaService mediaService, IEntityService entityService)
|
||||
{
|
||||
_contentService = contentService;
|
||||
_mediaService = mediaService;
|
||||
_entityService = entityService;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if the current user has access to save the user data
|
||||
/// </summary>
|
||||
/// <param name="currentUser">The current user trying to save user data</param>
|
||||
/// <param name="savingUser">The user instance being saved (can be null if it's a new user)</param>
|
||||
/// <param name="startContentIds">The start content ids of the user being saved (can be null or empty)</param>
|
||||
/// <param name="startMediaIds">The start media ids of the user being saved (can be null or empty)</param>
|
||||
/// <param name="userGroupAliases">The user aliases of the user being saved (can be null or empty)</param>
|
||||
/// <returns></returns>
|
||||
public Attempt<string> IsAuthorized(IUser currentUser,
|
||||
IUser savingUser,
|
||||
IEnumerable<int> startContentIds, IEnumerable<int> startMediaIds,
|
||||
IEnumerable<string> userGroupAliases)
|
||||
{
|
||||
var currentIsAdmin = currentUser.IsAdmin();
|
||||
|
||||
// a) A non-admin cannot save an admin
|
||||
|
||||
if (savingUser != null)
|
||||
{
|
||||
if (savingUser.IsAdmin() && currentIsAdmin == false)
|
||||
return Attempt.Fail("The current user is not an administrator so cannot save another administrator");
|
||||
}
|
||||
|
||||
// b) If a start node is changing, a user cannot set a start node on another user that they don't have access to, this even goes for admins
|
||||
|
||||
//only validate any start nodes that have changed.
|
||||
//a user can remove any start nodes and add start nodes that they have access to
|
||||
//but they cannot add a start node that they do not have access to
|
||||
|
||||
var changedStartContentIds = savingUser == null
|
||||
? startContentIds
|
||||
: startContentIds == null
|
||||
? null
|
||||
: startContentIds.Except(savingUser.StartContentIds).ToArray();
|
||||
var changedStartMediaIds = savingUser == null
|
||||
? startMediaIds
|
||||
: startMediaIds == null
|
||||
? null
|
||||
: startMediaIds.Except(savingUser.StartMediaIds).ToArray();
|
||||
var pathResult = AuthorizePath(currentUser, changedStartContentIds, changedStartMediaIds);
|
||||
if (pathResult == false)
|
||||
return pathResult;
|
||||
|
||||
// c) an admin can manage any group or section access
|
||||
|
||||
if (currentIsAdmin)
|
||||
return Attempt<string>.Succeed();
|
||||
|
||||
if (userGroupAliases != null)
|
||||
{
|
||||
var savingGroupAliases = userGroupAliases.ToArray();
|
||||
|
||||
//only validate any groups that have changed.
|
||||
//a non-admin user can remove groups and add groups that they have access to
|
||||
//but they cannot add a group that they do not have access to or that grants them
|
||||
//path or section access that they don't have access to.
|
||||
|
||||
var newGroups = savingUser == null
|
||||
? savingGroupAliases
|
||||
: savingGroupAliases.Except(savingUser.Groups.Select(x => x.Alias)).ToArray();
|
||||
|
||||
var userGroupsChanged = savingUser != null && newGroups.Length > 0;
|
||||
|
||||
if (userGroupsChanged)
|
||||
{
|
||||
// d) A user cannot assign a group to another user that they do not belong to
|
||||
|
||||
var currentUserGroups = currentUser.Groups.Select(x => x.Alias).ToArray();
|
||||
|
||||
foreach (var group in newGroups)
|
||||
{
|
||||
if (currentUserGroups.Contains(group) == false)
|
||||
{
|
||||
return Attempt.Fail("Cannot assign the group " + group + ", the current user is not a member");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Attempt<string>.Succeed();
|
||||
}
|
||||
|
||||
private Attempt<string> AuthorizePath(IUser currentUser, IEnumerable<int> startContentIds, IEnumerable<int> startMediaIds)
|
||||
{
|
||||
if (startContentIds != null)
|
||||
{
|
||||
foreach (var contentId in startContentIds)
|
||||
{
|
||||
if (contentId == Constants.System.Root)
|
||||
{
|
||||
var hasAccess = ContentPermissions.HasPathAccess("-1", currentUser.CalculateContentStartNodeIds(_entityService), Constants.System.RecycleBinContent);
|
||||
if (hasAccess == false)
|
||||
return Attempt.Fail("The current user does not have access to the content root");
|
||||
}
|
||||
else
|
||||
{
|
||||
var content = _contentService.GetById(contentId);
|
||||
if (content == null) continue;
|
||||
var hasAccess = currentUser.HasPathAccess(content, _entityService);
|
||||
if (hasAccess == false)
|
||||
return Attempt.Fail("The current user does not have access to the content path " + content.Path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (startMediaIds != null)
|
||||
{
|
||||
foreach (var mediaId in startMediaIds)
|
||||
{
|
||||
if (mediaId == Constants.System.Root)
|
||||
{
|
||||
var hasAccess = ContentPermissions.HasPathAccess("-1", currentUser.CalculateMediaStartNodeIds(_entityService), Constants.System.RecycleBinMedia);
|
||||
if (hasAccess == false)
|
||||
return Attempt.Fail("The current user does not have access to the media root");
|
||||
}
|
||||
else
|
||||
{
|
||||
var media = _mediaService.GetById(mediaId);
|
||||
if (media == null) continue;
|
||||
var hasAccess = currentUser.HasPathAccess(media, _entityService);
|
||||
if (hasAccess == false)
|
||||
return Attempt.Fail("The current user does not have access to the media path " + media.Path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Attempt<string>.Succeed();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
using Umbraco.Core.Composing;
|
||||
using Umbraco.Web.HealthCheck.NotificationMethods;
|
||||
|
||||
namespace Umbraco.Web.HealthCheck
|
||||
{
|
||||
public class HealthCheckNotificationMethodCollection : BuilderCollectionBase<IHealthCheckNotificationMethod>
|
||||
{
|
||||
public HealthCheckNotificationMethodCollection(IEnumerable<IHealthCheckNotificationMethod> items)
|
||||
: base(items)
|
||||
{ }
|
||||
}
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
using Umbraco.Core.Composing;
|
||||
using Umbraco.Web.HealthCheck.NotificationMethods;
|
||||
|
||||
namespace Umbraco.Web.HealthCheck
|
||||
{
|
||||
public class HealthCheckNotificationMethodCollectionBuilder : LazyCollectionBuilderBase<HealthCheckNotificationMethodCollectionBuilder, HealthCheckNotificationMethodCollection, IHealthCheckNotificationMethod>
|
||||
{
|
||||
protected override HealthCheckNotificationMethodCollectionBuilder This => this;
|
||||
}
|
||||
}
|
||||
@@ -1,183 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using HeyRed.MarkdownSharp;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Umbraco.Core;
|
||||
using Umbraco.Core.HealthCheck;
|
||||
|
||||
namespace Umbraco.Infrastructure.HealthCheck
|
||||
{
|
||||
public class HealthCheckResults
|
||||
{
|
||||
private readonly Dictionary<string, IEnumerable<HealthCheckStatus>> _results;
|
||||
public readonly bool AllChecksSuccessful;
|
||||
|
||||
private ILogger Logger => StaticApplicationLogging.Logger; // TODO: inject
|
||||
|
||||
public HealthCheckResults(IEnumerable<Core.HealthCheck.HealthCheck> checks)
|
||||
{
|
||||
_results = checks.ToDictionary(
|
||||
t => t.Name,
|
||||
t => {
|
||||
try
|
||||
{
|
||||
return t.GetStatus();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError(ex, "Error running scheduled health check: {HealthCheckName}", t.Name);
|
||||
var message = $"Health check failed with exception: {ex.Message}. See logs for details.";
|
||||
return new List<HealthCheckStatus>
|
||||
{
|
||||
new HealthCheckStatus(message)
|
||||
{
|
||||
ResultType = StatusResultType.Error
|
||||
}
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
// find out if all checks pass or not
|
||||
AllChecksSuccessful = true;
|
||||
foreach (var result in _results)
|
||||
{
|
||||
var checkIsSuccess = result.Value.All(x => x.ResultType == StatusResultType.Success || x.ResultType == StatusResultType.Info || x.ResultType == StatusResultType.Warning);
|
||||
if (checkIsSuccess == false)
|
||||
{
|
||||
AllChecksSuccessful = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void LogResults()
|
||||
{
|
||||
Logger.LogInformation("Scheduled health check results:");
|
||||
foreach (var result in _results)
|
||||
{
|
||||
var checkName = result.Key;
|
||||
var checkResults = result.Value;
|
||||
var checkIsSuccess = result.Value.All(x => x.ResultType == StatusResultType.Success);
|
||||
if (checkIsSuccess)
|
||||
{
|
||||
Logger.LogInformation("Checks for '{HealthCheckName}' all completed successfully.", checkName);
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.LogWarning("Checks for '{HealthCheckName}' completed with errors.", checkName);
|
||||
}
|
||||
|
||||
foreach (var checkResult in checkResults)
|
||||
{
|
||||
Logger.LogInformation("Result for {HealthCheckName}: {HealthCheckResult}, Message: '{HealthCheckMessage}'", checkName, checkResult.ResultType, checkResult.Message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public string ResultsAsMarkDown(HealthCheckNotificationVerbosity verbosity)
|
||||
{
|
||||
var newItem = "- ";
|
||||
|
||||
var sb = new StringBuilder();
|
||||
|
||||
foreach (var result in _results)
|
||||
{
|
||||
var checkName = result.Key;
|
||||
var checkResults = result.Value;
|
||||
var checkIsSuccess = result.Value.All(x => x.ResultType == StatusResultType.Success);
|
||||
|
||||
// add a new line if not the first check
|
||||
if (result.Equals(_results.First()) == false)
|
||||
{
|
||||
sb.Append(Environment.NewLine);
|
||||
}
|
||||
|
||||
if (checkIsSuccess)
|
||||
{
|
||||
sb.AppendFormat("{0}Checks for '{1}' all completed successfully.{2}", newItem, checkName, Environment.NewLine);
|
||||
}
|
||||
else
|
||||
{
|
||||
sb.AppendFormat("{0}Checks for '{1}' completed with errors.{2}", newItem, checkName, Environment.NewLine);
|
||||
}
|
||||
|
||||
foreach (var checkResult in checkResults)
|
||||
{
|
||||
sb.AppendFormat("\t{0}Result: '{1}'", newItem, checkResult.ResultType);
|
||||
|
||||
// With summary logging, only record details of warnings or errors
|
||||
if (checkResult.ResultType != StatusResultType.Success || verbosity == HealthCheckNotificationVerbosity.Detailed)
|
||||
{
|
||||
sb.AppendFormat(", Message: '{0}'", SimpleHtmlToMarkDown(checkResult.Message));
|
||||
}
|
||||
|
||||
sb.AppendLine(Environment.NewLine);
|
||||
}
|
||||
}
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
public string ResultsAsHtml(HealthCheckNotificationVerbosity verbosity)
|
||||
{
|
||||
var mark = new Markdown();
|
||||
var html = mark.Transform(ResultsAsMarkDown(verbosity));
|
||||
html = ApplyHtmlHighlighting(html);
|
||||
return html;
|
||||
}
|
||||
|
||||
internal Dictionary<string, IEnumerable<HealthCheckStatus>> ResultsAsDictionary => _results;
|
||||
|
||||
private string ApplyHtmlHighlighting(string html)
|
||||
{
|
||||
const string SuccessHexColor = "5cb85c";
|
||||
const string WarningHexColor = "f0ad4e";
|
||||
const string ErrorHexColor = "d9534f";
|
||||
|
||||
html = ApplyHtmlHighlightingForStatus(html, StatusResultType.Success, SuccessHexColor);
|
||||
html = ApplyHtmlHighlightingForStatus(html, StatusResultType.Warning, WarningHexColor);
|
||||
return ApplyHtmlHighlightingForStatus(html, StatusResultType.Error, ErrorHexColor);
|
||||
}
|
||||
|
||||
private string ApplyHtmlHighlightingForStatus(string html, StatusResultType status, string color)
|
||||
{
|
||||
return html
|
||||
.Replace("Result: '" + status + "'", "Result: <span style=\"color: #" + color + "\">" + status + "</span>");
|
||||
}
|
||||
|
||||
private string SimpleHtmlToMarkDown(string html)
|
||||
{
|
||||
return html.Replace("<strong>", "**")
|
||||
.Replace("</strong>", "**")
|
||||
.Replace("<em>", "*")
|
||||
.Replace("</em>", "*");
|
||||
}
|
||||
|
||||
public Dictionary<string, IEnumerable<HealthCheckStatus>> GetResultsForStatus(StatusResultType resultType)
|
||||
{
|
||||
switch (resultType)
|
||||
{
|
||||
case StatusResultType.Success:
|
||||
// a check is considered a success status if all checks are successful or info
|
||||
var successResults = _results.Where(x => x.Value.Any(y => y.ResultType == StatusResultType.Success) && x.Value.All(y => y.ResultType == StatusResultType.Success || y.ResultType == StatusResultType.Info));
|
||||
return successResults.ToDictionary(x => x.Key, x => x.Value);
|
||||
case StatusResultType.Warning:
|
||||
// a check is considered warn status if one check is warn and all others are success or info
|
||||
var warnResults = _results.Where(x => x.Value.Any(y => y.ResultType == StatusResultType.Warning) && x.Value.All(y => y.ResultType == StatusResultType.Warning || y.ResultType == StatusResultType.Success || y.ResultType == StatusResultType.Info));
|
||||
return warnResults.ToDictionary(x => x.Key, x => x.Value);
|
||||
case StatusResultType.Error:
|
||||
// a check is considered error status if any check is error
|
||||
var errorResults = _results.Where(x => x.Value.Any(y => y.ResultType == StatusResultType.Error));
|
||||
return errorResults.ToDictionary(x => x.Key, x => x.Value);
|
||||
case StatusResultType.Info:
|
||||
// a check is considered info status if all checks are info
|
||||
var infoResults = _results.Where(x => x.Value.All(y => y.ResultType == StatusResultType.Info));
|
||||
return infoResults.ToDictionary(x => x.Key, x => x.Value);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
using HeyRed.MarkdownSharp;
|
||||
using Umbraco.Core.HealthCheck;
|
||||
using Umbraco.Infrastructure.HealthCheck;
|
||||
using Umbraco.Web.HealthCheck.NotificationMethods;
|
||||
|
||||
namespace Umbraco.Web.HealthCheck
|
||||
{
|
||||
public class MarkdownToHtmlConverter : IMarkdownToHtmlConverter
|
||||
{
|
||||
public string ToHtml(HealthCheckResults results, HealthCheckNotificationVerbosity verbosity)
|
||||
{
|
||||
var mark = new Markdown();
|
||||
var html = mark.Transform(results.ResultsAsMarkDown(verbosity));
|
||||
html = ApplyHtmlHighlighting(html);
|
||||
return html;
|
||||
}
|
||||
|
||||
private string ApplyHtmlHighlighting(string html)
|
||||
{
|
||||
const string SuccessHexColor = "5cb85c";
|
||||
const string WarningHexColor = "f0ad4e";
|
||||
const string ErrorHexColor = "d9534f";
|
||||
|
||||
html = ApplyHtmlHighlightingForStatus(html, StatusResultType.Success, SuccessHexColor);
|
||||
html = ApplyHtmlHighlightingForStatus(html, StatusResultType.Warning, WarningHexColor);
|
||||
return ApplyHtmlHighlightingForStatus(html, StatusResultType.Error, ErrorHexColor);
|
||||
}
|
||||
|
||||
private string ApplyHtmlHighlightingForStatus(string html, StatusResultType status, string color)
|
||||
{
|
||||
return html
|
||||
.Replace("Result: '" + status + "'", "Result: <span style=\"color: #" + color + "\">" + status + "</span>");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,89 +0,0 @@
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Umbraco.Core;
|
||||
using Umbraco.Core.Configuration.Models;
|
||||
using Umbraco.Core.HealthCheck;
|
||||
using Umbraco.Core.Models;
|
||||
using Umbraco.Core.Services;
|
||||
using Umbraco.Infrastructure.HealthCheck;
|
||||
|
||||
namespace Umbraco.Web.HealthCheck.NotificationMethods
|
||||
{
|
||||
[HealthCheckNotificationMethod("email")]
|
||||
public class EmailNotificationMethod : NotificationMethodBase
|
||||
{
|
||||
private readonly ILocalizedTextService _textService;
|
||||
private readonly IRequestAccessor _requestAccessor;
|
||||
private readonly IEmailSender _emailSender;
|
||||
|
||||
private readonly ContentSettings _contentSettings;
|
||||
|
||||
public EmailNotificationMethod(
|
||||
ILocalizedTextService textService,
|
||||
IRequestAccessor requestAccessor,
|
||||
IEmailSender emailSender,
|
||||
IOptions<HealthChecksSettings> healthChecksSettings,
|
||||
IOptions<ContentSettings> contentSettings)
|
||||
: base(healthChecksSettings)
|
||||
{
|
||||
var recipientEmail = Settings?["RecipientEmail"];
|
||||
if (string.IsNullOrWhiteSpace(recipientEmail))
|
||||
{
|
||||
Enabled = false;
|
||||
return;
|
||||
}
|
||||
|
||||
RecipientEmail = recipientEmail;
|
||||
|
||||
_textService = textService ?? throw new ArgumentNullException(nameof(textService));
|
||||
_requestAccessor = requestAccessor;
|
||||
_emailSender = emailSender;
|
||||
_contentSettings = contentSettings.Value ?? throw new ArgumentNullException(nameof(contentSettings));
|
||||
}
|
||||
|
||||
public string RecipientEmail { get; }
|
||||
|
||||
public override async Task SendAsync(HealthCheckResults results)
|
||||
{
|
||||
if (ShouldSend(results) == false)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(RecipientEmail))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var message = _textService.Localize("healthcheck/scheduledHealthCheckEmailBody", new[]
|
||||
{
|
||||
DateTime.Now.ToShortDateString(),
|
||||
DateTime.Now.ToShortTimeString(),
|
||||
results.ResultsAsHtml(Verbosity)
|
||||
});
|
||||
|
||||
// Include the umbraco Application URL host in the message subject so that
|
||||
// you can identify the site that these results are for.
|
||||
var host = _requestAccessor.GetApplicationUrl();
|
||||
|
||||
var subject = _textService.Localize("healthcheck/scheduledHealthCheckEmailSubject", new[] { host.ToString() });
|
||||
|
||||
|
||||
var mailMessage = CreateMailMessage(subject, message);
|
||||
await _emailSender.SendAsync(mailMessage);
|
||||
}
|
||||
|
||||
private EmailMessage CreateMailMessage(string subject, string message)
|
||||
{
|
||||
var to = _contentSettings.Notifications.Email;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(subject))
|
||||
subject = "Umbraco Health Check Status";
|
||||
|
||||
var isBodyHtml = message.IsNullOrWhiteSpace() == false && message.Contains("<") && message.Contains("</");
|
||||
return new EmailMessage(to, RecipientEmail, subject, message, isBodyHtml);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Umbraco.Core.Composing;
|
||||
using Umbraco.Infrastructure.HealthCheck;
|
||||
|
||||
namespace Umbraco.Web.HealthCheck.NotificationMethods
|
||||
{
|
||||
public interface IHealthCheckNotificationMethod : IDiscoverable
|
||||
{
|
||||
bool Enabled { get; }
|
||||
|
||||
Task SendAsync(HealthCheckResults results);
|
||||
}
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Reflection;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Umbraco.Core.Configuration.Models;
|
||||
using Umbraco.Core.HealthCheck;
|
||||
using Umbraco.Infrastructure.HealthCheck;
|
||||
|
||||
namespace Umbraco.Web.HealthCheck.NotificationMethods
|
||||
{
|
||||
public abstract class NotificationMethodBase : IHealthCheckNotificationMethod
|
||||
{
|
||||
protected NotificationMethodBase(IOptions<HealthChecksSettings> healthCheckSettings)
|
||||
{
|
||||
var type = GetType();
|
||||
var attribute = type.GetCustomAttribute<HealthCheckNotificationMethodAttribute>();
|
||||
if (attribute == null)
|
||||
{
|
||||
Enabled = false;
|
||||
return;
|
||||
}
|
||||
|
||||
var notificationMethods = healthCheckSettings.Value.Notification.NotificationMethods;
|
||||
if (!notificationMethods.TryGetValue(attribute.Alias, out var notificationMethod))
|
||||
{
|
||||
Enabled = false;
|
||||
return;
|
||||
}
|
||||
|
||||
Enabled = notificationMethod.Enabled;
|
||||
FailureOnly = notificationMethod.FailureOnly;
|
||||
Verbosity = notificationMethod.Verbosity;
|
||||
Settings = notificationMethod.Settings;
|
||||
}
|
||||
|
||||
public bool Enabled { get; protected set; }
|
||||
|
||||
public bool FailureOnly { get; protected set; }
|
||||
|
||||
public HealthCheckNotificationVerbosity Verbosity { get; protected set; }
|
||||
|
||||
public IDictionary<string, string> Settings { get; }
|
||||
|
||||
protected bool ShouldSend(HealthCheckResults results)
|
||||
{
|
||||
return Enabled && (!FailureOnly || !results.AllChecksSuccessful);
|
||||
}
|
||||
|
||||
public abstract Task SendAsync(HealthCheckResults results);
|
||||
}
|
||||
}
|
||||
@@ -5,12 +5,12 @@ using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Umbraco.Core;
|
||||
using Umbraco.Core.Configuration;
|
||||
using Umbraco.Core.Configuration.Extensions;
|
||||
using Umbraco.Core.Configuration.Models;
|
||||
using Umbraco.Core.HealthCheck;
|
||||
using Umbraco.Core.Logging;
|
||||
using Umbraco.Core.Scoping;
|
||||
using Umbraco.Core.Sync;
|
||||
using Umbraco.Infrastructure.Configuration.Extensions;
|
||||
using Umbraco.Infrastructure.HealthCheck;
|
||||
using Umbraco.Web.HealthCheck;
|
||||
|
||||
|
||||
@@ -8,7 +8,6 @@ using System.Xml.XPath;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Umbraco.Core.Collections;
|
||||
using Umbraco.Core.Configuration;
|
||||
using Umbraco.Core.Configuration.Models;
|
||||
using Umbraco.Core.Models;
|
||||
using Umbraco.Core.Models.Entities;
|
||||
@@ -17,7 +16,6 @@ using Umbraco.Core.PropertyEditors;
|
||||
using Umbraco.Core.Scoping;
|
||||
using Umbraco.Core.Serialization;
|
||||
using Umbraco.Core.Services;
|
||||
using Umbraco.Core.Services.Implement;
|
||||
using Umbraco.Core.Strings;
|
||||
|
||||
namespace Umbraco.Core.Packaging
|
||||
|
||||
@@ -60,9 +60,6 @@ using IntegerValidator = Umbraco.Core.PropertyEditors.Validators.IntegerValidato
|
||||
using TextStringValueConverter = Umbraco.Core.PropertyEditors.ValueConverters.TextStringValueConverter;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Umbraco.Core.DependencyInjection;
|
||||
using Umbraco.Core.Configuration.HealthChecks;
|
||||
using Umbraco.Core.HealthCheck;
|
||||
using Umbraco.Core.HealthCheck.Checks;
|
||||
using Umbraco.Core.Security;
|
||||
|
||||
namespace Umbraco.Core.Runtime
|
||||
@@ -303,6 +300,7 @@ namespace Umbraco.Core.Runtime
|
||||
builder.Services.AddUnique<IPublishedRouter, PublishedRouter>();
|
||||
|
||||
// register *all* checks, except those marked [HideFromTypeFinder] of course
|
||||
builder.Services.AddUnique<IMarkdownToHtmlConverter, MarkdownToHtmlConverter>();
|
||||
builder.HealthChecks()
|
||||
.Add(() => builder.TypeLoader.GetTypes<HealthCheck.HealthCheck>());
|
||||
|
||||
|
||||
Reference in New Issue
Block a user