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:
Bjarke Berg
2020-12-04 13:01:58 +01:00
parent 05dc597fc2
commit 21e3cf0887
18 changed files with 63 additions and 42 deletions

View File

@@ -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;
}
}
}
}

View File

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

View File

@@ -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;
}
}
}
}

View File

@@ -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;
});
}
}
}
}

View File

@@ -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);
}
}
}

View File

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

View File

@@ -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)
{ }
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}
}

View File

@@ -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>");
}
}
}

View File

@@ -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);
}
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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;

View File

@@ -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

View File

@@ -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>());