Merge remote-tracking branch 'Umbraco/netcore/dev' into netcore/feature/users

This commit is contained in:
Scott Brady
2020-04-22 15:58:54 +01:00
102 changed files with 2115 additions and 700 deletions
-1
View File
@@ -37,4 +37,3 @@ Besides "Our", we all support each other also via Twitter: [Umbraco HQ](https://
## Contributing
Umbraco is contribution-focused and community-driven. If you want to contribute back to the Umbraco source code, please check out our [guide to contributing](CONTRIBUTING.md).
+1
View File
@@ -176,3 +176,4 @@ build/temp/
/src/Umbraco.Web.UI.NetCore/wwwroot/Umbraco/lib/*
/src/Umbraco.Web.UI.NetCore/wwwroot/Umbraco/views/*
/src/Umbraco.Web.UI.NetCore/wwwroot/App_Data/TEMP/*
/src/Umbraco.Web.UI.NetCore/App_Data/Logs/*
@@ -0,0 +1,190 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using Umbraco.Core.Composing;
namespace Umbraco.Core.Cache
{
/// <summary>
/// Implements a fast <see cref="IAppCache"/> on top of HttpContext.Items.
/// </summary>
/// <remarks>
/// <para>If no current HttpContext items can be found (no current HttpContext,
/// or no Items...) then this cache acts as a pass-through and does not cache
/// anything.</para>
/// </remarks>
public class GenericDictionaryRequestAppCache : FastDictionaryAppCacheBase, IRequestCache
{
/// <summary>
/// Initializes a new instance of the <see cref="HttpRequestAppCache"/> class with a context, for unit tests!
/// </summary>
public GenericDictionaryRequestAppCache(Func<IDictionary<object, object>> requestItems) : base()
{
ContextItems = requestItems;
}
private Func<IDictionary<object, object>> ContextItems { get; }
public bool IsAvailable => TryGetContextItems(out _);
private bool TryGetContextItems(out IDictionary<object, object> items)
{
items = ContextItems?.Invoke();
return items != null;
}
/// <inheritdoc />
public override object Get(string key, Func<object> factory)
{
//no place to cache so just return the callback result
if (!TryGetContextItems(out var items)) return factory();
key = GetCacheKey(key);
Lazy<object> result;
try
{
EnterWriteLock();
result = items[key] as Lazy<object>; // null if key not found
// cannot create value within the lock, so if result.IsValueCreated is false, just
// do nothing here - means that if creation throws, a race condition could cause
// more than one thread to reach the return statement below and throw - accepted.
if (result == null || SafeLazy.GetSafeLazyValue(result, true) == null) // get non-created as NonCreatedValue & exceptions as null
{
result = SafeLazy.GetSafeLazy(factory);
items[key] = result;
}
}
finally
{
ExitWriteLock();
}
// using GetSafeLazy and GetSafeLazyValue ensures that we don't cache
// exceptions (but try again and again) and silently eat them - however at
// some point we have to report them - so need to re-throw here
// this does not throw anymore
//return result.Value;
var value = result.Value; // will not throw (safe lazy)
if (value is SafeLazy.ExceptionHolder eh) eh.Exception.Throw(); // throw once!
return value;
}
public bool Set(string key, object value)
{
//no place to cache so just return the callback result
if (!TryGetContextItems(out var items)) return false;
key = GetCacheKey(key);
try
{
EnterWriteLock();
items[key] = SafeLazy.GetSafeLazy(() => value);
}
finally
{
ExitWriteLock();
}
return true;
}
public bool Remove(string key)
{
//no place to cache so just return the callback result
if (!TryGetContextItems(out var items)) return false;
key = GetCacheKey(key);
try
{
EnterWriteLock();
items.Remove(key);
}
finally
{
ExitWriteLock();
}
return true;
}
#region Entries
protected override IEnumerable<DictionaryEntry> GetDictionaryEntries()
{
const string prefix = CacheItemPrefix + "-";
if (!TryGetContextItems(out var items)) return Enumerable.Empty<DictionaryEntry>();
return items.Cast<DictionaryEntry>()
.Where(x => x.Key is string s && s.StartsWith(prefix));
}
protected override void RemoveEntry(string key)
{
if (!TryGetContextItems(out var items)) return;
items.Remove(key);
}
protected override object GetEntry(string key)
{
return !TryGetContextItems(out var items) ? null : items[key];
}
#endregion
#region Lock
private const string ContextItemsLockKey = "Umbraco.Core.Cache.HttpRequestCache::LockEntered";
protected override void EnterReadLock() => EnterWriteLock();
protected override void EnterWriteLock()
{
if (!TryGetContextItems(out var items)) return;
// note: cannot keep 'entered' as a class variable here,
// since there is one per request - so storing it within
// ContextItems - which is locked, so this should be safe
var entered = false;
Monitor.Enter(items, ref entered);
items[ContextItemsLockKey] = entered;
}
protected override void ExitReadLock() => ExitWriteLock();
protected override void ExitWriteLock()
{
if (!TryGetContextItems(out var items)) return;
var entered = (bool?)items[ContextItemsLockKey] ?? false;
if (entered)
Monitor.Exit(items);
items.Remove(ContextItemsLockKey);
}
#endregion
public IEnumerator<KeyValuePair<string, object>> GetEnumerator()
{
if (!TryGetContextItems(out var items))
{
yield break;
}
foreach (var item in items)
{
yield return new KeyValuePair<string, object>(item.Key.ToString(), item.Value);
}
}
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}
}
@@ -0,0 +1,20 @@
namespace Umbraco.Core
{
public static partial class Constants
{
public static class SqlTemplates
{
public static class VersionableRepository
{
public const string GetVersionIds = "Umbraco.Core.VersionableRepository.GetVersionIds";
public const string GetVersion = "Umbraco.Core.VersionableRepository.GetVersion";
public const string GetVersions = "Umbraco.Core.VersionableRepository.GetVersions";
public const string EnsureUniqueNodeName = "Umbraco.Core.VersionableRepository.EnsureUniqueNodeName";
public const string GetSortOrder = "Umbraco.Core.VersionableRepository.GetSortOrder";
public const string GetParentNode = "Umbraco.Core.VersionableRepository.GetParentNode";
public const string GetReservedId = "Umbraco.Core.VersionableRepository.GetReservedId";
}
}
}
}
+1
View File
@@ -6,6 +6,7 @@ namespace Umbraco.Core
{
public static class ContentExtensions
{
#region XML methods
/// <summary>
@@ -6,6 +6,10 @@ namespace Umbraco.Core.Hosting
{
string SiteName { get; }
string ApplicationId { get; }
/// <summary>
/// Will return the physical path to the root of the application
/// </summary>
string ApplicationPhysicalPath { get; }
string LocalTempPath { get; }
@@ -27,10 +31,22 @@ namespace Umbraco.Core.Hosting
bool IsHosted { get; }
Version IISVersion { get; }
// TODO: Should we change this name to MapPathWebRoot ? and also have a new MapPathContentRoot ?
/// <summary>
/// Maps a virtual path to a physical path to the application's web root
/// </summary>
/// <param name="path"></param>
/// <returns></returns>
/// <remarks>
/// Depending on the runtime 'web root', this result can vary. For example in Net Framework the web root and the content root are the same, however
/// in netcore the web root is /www therefore this will Map to a physical path within www.
/// </remarks>
string MapPath(string path);
/// <summary>
/// Maps a virtual path to the application's web root
/// Converts a virtual path to an absolute URL path based on the application's web root
/// </summary>
/// <param name="virtualPath">The virtual path. Must start with either ~/ or / else an exception is thrown.</param>
/// <returns></returns>
@@ -0,0 +1,13 @@
namespace Umbraco.Core.Logging
{
public interface ILoggingConfiguration
{
/// <summary>
/// The physical path where logs are stored
/// </summary>
string LogDirectory { get; }
string LogConfigurationFile { get; }
string UserLogConfigurationFile { get; }
}
}
+1 -7
View File
@@ -2,18 +2,12 @@
namespace Umbraco.Core.Logging
{
/// <summary>
/// Defines the profiling service.
/// </summary>
public interface IProfiler
{
/// <summary>
/// Renders the profiling results.
/// </summary>
/// <returns>The profiling results.</returns>
/// <remarks>Generally used for HTML rendering.</remarks>
string Render();
/// <summary>
/// Gets an <see cref="IDisposable"/> that will time the code between its creation and disposal.
/// </summary>
+15
View File
@@ -0,0 +1,15 @@
namespace Umbraco.Core.Logging
{
/// <summary>
/// Used to render a profiler in a web page
/// </summary>
public interface IProfilerHtml
{
/// <summary>
/// Renders the profiling results.
/// </summary>
/// <returns>The profiling results.</returns>
/// <remarks>Generally used for HTML rendering.</remarks>
string Render();
}
}
-6
View File
@@ -15,12 +15,6 @@ namespace Umbraco.Core.Logging
_logger = logger;
}
/// <inheritdoc/>
public string Render()
{
return string.Empty;
}
/// <inheritdoc/>
public IDisposable Step(string name)
{
@@ -0,0 +1,20 @@
using System;
namespace Umbraco.Core.Logging
{
public class LoggingConfiguration : ILoggingConfiguration
{
public LoggingConfiguration(string logDirectory, string logConfigurationFile, string userLogConfigurationFile)
{
LogDirectory = logDirectory ?? throw new ArgumentNullException(nameof(logDirectory));
LogConfigurationFile = logConfigurationFile ?? throw new ArgumentNullException(nameof(logConfigurationFile));
UserLogConfigurationFile = userLogConfigurationFile ?? throw new ArgumentNullException(nameof(userLogConfigurationFile));
}
public string LogDirectory { get; }
public string LogConfigurationFile { get; }
public string UserLogConfigurationFile { get; }
}
}
-5
View File
@@ -6,11 +6,6 @@ namespace Umbraco.Core.Logging
{
private readonly VoidDisposable _disposable = new VoidDisposable();
public string Render()
{
return string.Empty;
}
public IDisposable Step(string name)
{
return _disposable;
@@ -0,0 +1,48 @@
using System.Collections.Generic;
using System.Linq;
namespace Umbraco.Core.Models
{
public class ContentDataIntegrityReport
{
public ContentDataIntegrityReport(IReadOnlyDictionary<int, ContentDataIntegrityReportEntry> detectedIssues)
{
DetectedIssues = detectedIssues;
}
public bool Ok => DetectedIssues.Count == 0 || DetectedIssues.Count == DetectedIssues.Values.Count(x => x.Fixed);
public IReadOnlyDictionary<int, ContentDataIntegrityReportEntry> DetectedIssues { get; }
public IReadOnlyDictionary<int, ContentDataIntegrityReportEntry> FixedIssues
=> DetectedIssues.Where(x => x.Value.Fixed).ToDictionary(x => x.Key, x => x.Value);
public enum IssueType
{
/// <summary>
/// The item's level and path are inconsistent with it's parent's path and level
/// </summary>
InvalidPathAndLevelByParentId,
/// <summary>
/// The item's path doesn't contain all required parts
/// </summary>
InvalidPathEmpty,
/// <summary>
/// The item's path parts are inconsistent with it's level value
/// </summary>
InvalidPathLevelMismatch,
/// <summary>
/// The item's path does not end with it's own ID
/// </summary>
InvalidPathById,
/// <summary>
/// The item's path does not have it's parent Id as the 2nd last entry
/// </summary>
InvalidPathByParentId,
}
}
}
@@ -0,0 +1,13 @@
namespace Umbraco.Core.Models
{
public class ContentDataIntegrityReportEntry
{
public ContentDataIntegrityReportEntry(ContentDataIntegrityReport.IssueType issueType)
{
IssueType = issueType;
}
public ContentDataIntegrityReport.IssueType IssueType { get; }
public bool Fixed { get; set; }
}
}
@@ -0,0 +1,13 @@
namespace Umbraco.Core.Models
{
public class ContentDataIntegrityReportOptions
{
/// <summary>
/// Set to true to try to automatically resolve data integrity issues
/// </summary>
public bool FixIssues { get; set; }
// TODO: We could define all sorts of options for the data integrity check like what to check for, what to fix, etc...
// things like Tag data consistency, etc...
}
}
@@ -0,0 +1,7 @@
namespace Umbraco.Net
{
public class NullSessionIdResolver : ISessionIdResolver
{
public string SessionId => null;
}
}
@@ -1,9 +1,16 @@
namespace Umbraco.Core.Services
using Umbraco.Core.Models;
namespace Umbraco.Core.Services
{
/// <summary>
/// Placeholder for sharing logic between the content, media (and member) services
/// TODO: Start sharing the logic!
/// </summary>
public interface IContentServiceBase : IService
{ }
{
/// <summary>
/// Checks/fixes the data integrity of node paths/levels stored in the database
/// </summary>
ContentDataIntegrityReport CheckDataIntegrity(ContentDataIntegrityReportOptions options);
}
}
@@ -13,8 +13,17 @@ namespace Umbraco.Core.Composing
/// <param name="builder"></param>
/// <returns></returns>
public static IHostBuilder UseUmbraco(this IHostBuilder builder)
=> builder.UseUmbraco(new UmbracoServiceProviderFactory());
{
return builder
.UseUmbraco(new UmbracoServiceProviderFactory());
}
/// <summary>
/// Assigns a custom service provider factory to use Umbraco's container
/// </summary>
/// <param name="builder"></param>
/// <param name="umbracoServiceProviderFactory"></param>
/// <returns></returns>
public static IHostBuilder UseUmbraco(this IHostBuilder builder, UmbracoServiceProviderFactory umbracoServiceProviderFactory)
=> builder.UseServiceProviderFactory(umbracoServiceProviderFactory);
}
@@ -4,11 +4,9 @@ using System.IO;
using System.Linq;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Umbraco.Composing;
using Umbraco.Core.IO;
using Umbraco.Core.Models;
using Umbraco.Core.Models.Membership;
using Umbraco.Core.PropertyEditors;
using Umbraco.Core.Services;
using Umbraco.Core.Strings;
@@ -16,6 +14,20 @@ namespace Umbraco.Core
{
public static class ContentExtensions
{
internal static bool IsMoving(this IContentBase entity)
{
// Check if this entity is being moved as a descendant as part of a bulk moving operations.
// When this occurs, only Path + Level + UpdateDate are being changed. In this case we can bypass a lot of the below
// operations which will make this whole operation go much faster. When moving we don't need to create
// new versions, etc... because we cannot roll this operation back anyways.
var isMoving = entity.IsPropertyDirty(nameof(entity.Path))
&& entity.IsPropertyDirty(nameof(entity.Level))
&& entity.IsPropertyDirty(nameof(entity.UpdateDate));
return isMoving;
}
/// <summary>
/// Removes characters that are not valid XML characters from all entity properties
/// of type string. See: http://stackoverflow.com/a/961504/5018
@@ -3,6 +3,7 @@ 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
@@ -100,7 +101,7 @@ namespace Umbraco.Core.Diagnostics
return bRet;
}
public static bool Dump(IMarchal marchal, IIOHelper ioHelper, Option options = Option.WithFullMemory, bool withException = false)
public static bool Dump(IMarchal marchal, IHostingEnvironment hostingEnvironment, Option options = Option.WithFullMemory, bool withException = false)
{
lock (LockO)
{
@@ -110,7 +111,7 @@ namespace Umbraco.Core.Diagnostics
// filter everywhere in our code = not!
var stacktrace = withException ? Environment.StackTrace : string.Empty;
var filepath = ioHelper.MapPath("~/App_Data/MiniDump");
var filepath = Path.Combine(hostingEnvironment.ApplicationPhysicalPath, "App_Data/MiniDump");
if (Directory.Exists(filepath) == false)
Directory.CreateDirectory(filepath);
@@ -122,11 +123,11 @@ namespace Umbraco.Core.Diagnostics
}
}
public static bool OkToDump(IIOHelper ioHelper)
public static bool OkToDump(IHostingEnvironment hostingEnvironment)
{
lock (LockO)
{
var filepath = ioHelper.MapPath("~/App_Data/MiniDump");
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;
@@ -16,14 +16,14 @@ namespace Umbraco.Core.Logging
// but it only has a pre-release NuGet package. So, we've got to use Serilog's code, which
// means we cannot get rid of Serilog entirely. We may want to revisit this at some point.
// TODO: Do we still need this, is there a non-pre release package shipped?
private static readonly Lazy<global::Serilog.ILogger> MinimalLogger = new Lazy<global::Serilog.ILogger>(() => new LoggerConfiguration().CreateLogger());
public string Render(string messageTemplate, params object[] args)
{
// by default, unless initialized otherwise, Log.Logger is SilentLogger which cannot bind message
// templates. Log.Logger is set to a true Logger when initializing Umbraco's logger, but in case
// that has not been done already - use a temp minimal logger (eg for tests).
var logger = Log.Logger as global::Serilog.Core.Logger ?? MinimalLogger.Value;
// resolve a minimal logger instance which is used to bind message templates
var logger = MinimalLogger.Value;
var bound = logger.BindMessageTemplate(messageTemplate, args, out var parsedTemplate, out var boundProperties);
@@ -11,13 +11,13 @@ namespace Umbraco.Core.Logging.Serilog.Enrichers
/// Original source - https://github.com/serilog-web/classic/blob/master/src/SerilogWeb.Classic/Classic/Enrichers/HttpRequestIdEnricher.cs
/// Nupkg: 'Serilog.Web.Classic' contains handlers & extra bits we do not want
/// </summary>
internal class HttpRequestIdEnricher : ILogEventEnricher
public class HttpRequestIdEnricher : ILogEventEnricher
{
private readonly Func<IRequestCache> _requestCacheGetter;
private readonly IRequestCache _requestCache;
public HttpRequestIdEnricher(Func<IRequestCache> requestCacheGetter)
public HttpRequestIdEnricher(IRequestCache requestCache)
{
_requestCacheGetter = requestCacheGetter;
_requestCache = requestCache ?? throw new ArgumentNullException(nameof(requestCache));
}
/// <summary>
@@ -34,11 +34,8 @@ namespace Umbraco.Core.Logging.Serilog.Enrichers
{
if (logEvent == null) throw new ArgumentNullException(nameof(logEvent));
var requestCache = _requestCacheGetter();
if(requestCache is null) return;
Guid requestId;
if (!LogHttpRequest.TryGetCurrentHttpRequestId(out requestId, requestCache))
if (!LogHttpRequest.TryGetCurrentHttpRequestId(out requestId, _requestCache))
return;
var requestIdProperty = new LogEventProperty(HttpRequestIdPropertyName, new ScalarValue(requestId));
@@ -13,9 +13,9 @@ namespace Umbraco.Core.Logging.Serilog.Enrichers
/// Original source - https://github.com/serilog-web/classic/blob/master/src/SerilogWeb.Classic/Classic/Enrichers/HttpRequestNumberEnricher.cs
/// Nupkg: 'Serilog.Web.Classic' contains handlers & extra bits we do not want
/// </summary>
internal class HttpRequestNumberEnricher : ILogEventEnricher
public class HttpRequestNumberEnricher : ILogEventEnricher
{
private readonly Func<IRequestCache> _requestCacheGetter;
private readonly IRequestCache _requestCache;
private static int _lastRequestNumber;
private static readonly string _requestNumberItemName = typeof(HttpRequestNumberEnricher).Name + "+RequestNumber";
@@ -25,9 +25,9 @@ namespace Umbraco.Core.Logging.Serilog.Enrichers
private const string _httpRequestNumberPropertyName = "HttpRequestNumber";
public HttpRequestNumberEnricher(Func<IRequestCache> requestCacheGetter)
public HttpRequestNumberEnricher(IRequestCache requestCache)
{
_requestCacheGetter = requestCacheGetter;
_requestCache = requestCache ?? throw new ArgumentNullException(nameof(requestCache));
}
/// <summary>
@@ -39,10 +39,7 @@ namespace Umbraco.Core.Logging.Serilog.Enrichers
{
if (logEvent == null) throw new ArgumentNullException(nameof(logEvent));
var requestCache = _requestCacheGetter();
if (requestCache is null) return;
var requestNumber = requestCache.Get(_requestNumberItemName,
var requestNumber = _requestCache.Get(_requestNumberItemName,
() => Interlocked.Increment(ref _lastRequestNumber));
var requestNumberProperty = new LogEventProperty(_httpRequestNumberPropertyName, new ScalarValue(requestNumber));
@@ -10,7 +10,7 @@ namespace Umbraco.Core.Logging.Serilog.Enrichers
/// Original source - https://github.com/serilog-web/classic/blob/master/src/SerilogWeb.Classic/Classic/Enrichers/HttpSessionIdEnricher.cs
/// Nupkg: 'Serilog.Web.Classic' contains handlers & extra bits we do not want
/// </summary>
internal class HttpSessionIdEnricher : ILogEventEnricher
public class HttpSessionIdEnricher : ILogEventEnricher
{
private readonly ISessionIdResolver _sessionIdResolver;
@@ -0,0 +1,96 @@
using System;
using System.Reflection;
using System.Threading;
using Serilog.Core;
using Serilog.Events;
using Umbraco.Core.Configuration;
using Umbraco.Core.Diagnostics;
using Umbraco.Core.Hosting;
namespace Umbraco.Infrastructure.Logging.Serilog.Enrichers
{
/// <summary>
/// Enriches the log if there are ThreadAbort exceptions and will automatically create a minidump if it can
/// </summary>
public class ThreadAbortExceptionEnricher : ILogEventEnricher
{
private readonly ICoreDebugSettings _coreDebugSettings;
private readonly IHostingEnvironment _hostingEnvironment;
private readonly IMarchal _marchal;
public ThreadAbortExceptionEnricher(ICoreDebugSettings coreDebugSettings, IHostingEnvironment hostingEnvironment, IMarchal marchal)
{
_coreDebugSettings = coreDebugSettings;
_hostingEnvironment = hostingEnvironment;
_marchal = marchal;
}
public void Enrich(LogEvent logEvent, ILogEventPropertyFactory propertyFactory)
{
switch (logEvent.Level)
{
case LogEventLevel.Error:
case LogEventLevel.Fatal:
DumpThreadAborts(logEvent, propertyFactory);
break;
}
}
private void DumpThreadAborts(LogEvent logEvent, ILogEventPropertyFactory propertyFactory)
{
if (!IsTimeoutThreadAbortException(logEvent.Exception)) return;
var message = "The thread has been aborted, because the request has timed out.";
// dump if configured, or if stacktrace contains Monitor.ReliableEnter
var dump = _coreDebugSettings.DumpOnTimeoutThreadAbort || IsMonitorEnterThreadAbortException(logEvent.Exception);
// dump if it is ok to dump (might have a cap on number of dump...)
dump &= MiniDump.OkToDump(_hostingEnvironment);
if (!dump)
{
message += ". No minidump was created.";
logEvent.AddPropertyIfAbsent(propertyFactory.CreateProperty("ThreadAbortExceptionInfo", message));
}
else
try
{
var dumped = MiniDump.Dump(_marchal, _hostingEnvironment, withException: true);
message += dumped
? ". A minidump was created in App_Data/MiniDump."
: ". Failed to create a minidump.";
logEvent.AddPropertyIfAbsent(propertyFactory.CreateProperty("ThreadAbortExceptionInfo", message));
}
catch (Exception ex)
{
message = "Failed to create a minidump. " + ex;
logEvent.AddPropertyIfAbsent(propertyFactory.CreateProperty("ThreadAbortExceptionInfo", message));
}
}
private static bool IsTimeoutThreadAbortException(Exception exception)
{
if (!(exception is ThreadAbortException abort)) return false;
if (abort.ExceptionState == null) return false;
var stateType = abort.ExceptionState.GetType();
if (stateType.FullName != "System.Web.HttpApplication+CancelModuleException") return false;
var timeoutField = stateType.GetField("_timeout", BindingFlags.Instance | BindingFlags.NonPublic);
if (timeoutField == null) return false;
return (bool)timeoutField.GetValue(abort.ExceptionState);
}
private static bool IsMonitorEnterThreadAbortException(Exception exception)
{
if (!(exception is ThreadAbortException abort)) return false;
var stacktrace = abort.StackTrace;
return stacktrace.Contains("System.Threading.Monitor.ReliableEnter");
}
}
}
@@ -1,4 +1,5 @@
using System;
using System.IO;
using System.Text;
using Serilog;
using Serilog.Configuration;
@@ -6,11 +7,8 @@ using Serilog.Core;
using Serilog.Events;
using Serilog.Formatting;
using Serilog.Formatting.Compact;
using Umbraco.Core.Cache;
using Umbraco.Core.Composing;
using Umbraco.Core.Hosting;
using Umbraco.Core.Logging.Serilog.Enrichers;
using Umbraco.Net;
namespace Umbraco.Core.Logging.Serilog
{
@@ -24,27 +22,30 @@ namespace Umbraco.Core.Logging.Serilog
/// It is highly recommended that you keep/use this default in your own logging config customizations
/// </summary>
/// <param name="logConfig">A Serilog LoggerConfiguration</param>
/// <param name="hostingEnvironment"></param>
public static LoggerConfiguration MinimalConfiguration(this LoggerConfiguration logConfig, IHostingEnvironment hostingEnvironment, ISessionIdResolver sessionIdResolver, Func<IRequestCache> requestCacheGetter)
/// <param name="IHostingEnvironment"></param>
/// <param name="loggingConfiguration"></param>
public static LoggerConfiguration MinimalConfiguration(
this LoggerConfiguration logConfig,
IHostingEnvironment hostingEnvironment,
ILoggingConfiguration loggingConfiguration)
{
global::Serilog.Debugging.SelfLog.Enable(msg => System.Diagnostics.Debug.WriteLine(msg));
//Set this environment variable - so that it can be used in external config file
//add key="serilog:write-to:RollingFile.pathFormat" value="%BASEDIR%\logs\log.txt" />
Environment.SetEnvironmentVariable("BASEDIR", AppDomain.CurrentDomain.BaseDirectory, EnvironmentVariableTarget.Process);
Environment.SetEnvironmentVariable("MACHINENAME", Environment.MachineName, EnvironmentVariableTarget.Process);
Environment.SetEnvironmentVariable("UMBLOGDIR", loggingConfiguration.LogDirectory, EnvironmentVariableTarget.Process);
Environment.SetEnvironmentVariable("BASEDIR", hostingEnvironment.ApplicationPhysicalPath, EnvironmentVariableTarget.Process);
Environment.SetEnvironmentVariable("MACHINENAME", Environment.MachineName, EnvironmentVariableTarget.Process);
logConfig.MinimumLevel.Verbose() //Set to highest level of logging (as any sinks may want to restrict it to Errors only)
.Enrich.WithProcessId()
.Enrich.WithProcessName()
.Enrich.WithThreadId()
.Enrich.WithProperty(AppDomainId, AppDomain.CurrentDomain.Id)
.Enrich.WithProperty(AppDomainId, AppDomain.CurrentDomain.Id)
.Enrich.WithProperty("AppDomainAppId", hostingEnvironment.ApplicationId.ReplaceNonAlphanumericChars(string.Empty))
.Enrich.WithProperty("MachineName", Environment.MachineName)
.Enrich.With<Log4NetLevelMapperEnricher>()
.Enrich.With(new HttpSessionIdEnricher(sessionIdResolver))
.Enrich.With(new HttpRequestNumberEnricher(requestCacheGetter))
.Enrich.With(new HttpRequestIdEnricher(requestCacheGetter));
.Enrich.FromLogContext(); // allows us to dynamically enrich
return logConfig;
}
@@ -53,13 +54,14 @@ namespace Umbraco.Core.Logging.Serilog
/// Outputs a .txt format log at /App_Data/Logs/
/// </summary>
/// <param name="logConfig">A Serilog LoggerConfiguration</param>
/// <param name="loggingConfiguration"></param>
/// <param name="minimumLevel">The log level you wish the JSON file to collect - default is Verbose (highest)</param>
/// <param name="retainedFileCount">The number of days to keep log files. Default is set to null which means all logs are kept</param>
public static LoggerConfiguration OutputDefaultTextFile(this LoggerConfiguration logConfig, LogEventLevel minimumLevel = LogEventLevel.Verbose, int? retainedFileCount = null)
public static LoggerConfiguration OutputDefaultTextFile(this LoggerConfiguration logConfig, ILoggingConfiguration loggingConfiguration, LogEventLevel minimumLevel = LogEventLevel.Verbose, int? retainedFileCount = null)
{
//Main .txt logfile - in similar format to older Log4Net output
//Ends with ..txt as Date is inserted before file extension substring
logConfig.WriteTo.File($@"{AppDomain.CurrentDomain.BaseDirectory}\App_Data\Logs\UmbracoTraceLog.{Environment.MachineName}..txt",
logConfig.WriteTo.File(Path.Combine(loggingConfiguration.LogDirectory, $@"UmbracoTraceLog.{Environment.MachineName}..txt"),
shared: true,
rollingInterval: RollingInterval.Day,
restrictedToMinimumLevel: minimumLevel,
@@ -99,7 +101,8 @@ namespace Umbraco.Core.Logging.Serilog
rollingInterval,
rollOnFileSizeLimit,
retainedFileCountLimit,
encoding),
encoding,
null),
sinkMapCountLimit:0)
);
}
@@ -109,13 +112,14 @@ namespace Umbraco.Core.Logging.Serilog
/// Outputs a CLEF format JSON log at /App_Data/Logs/
/// </summary>
/// <param name="logConfig">A Serilog LoggerConfiguration</param>
/// <param name="loggingConfiguration"></param>
/// <param name="minimumLevel">The log level you wish the JSON file to collect - default is Verbose (highest)</param>
/// <param name="retainedFileCount">The number of days to keep log files. Default is set to null which means all logs are kept</param>
public static LoggerConfiguration OutputDefaultJsonFile(this LoggerConfiguration logConfig, LogEventLevel minimumLevel = LogEventLevel.Verbose, int? retainedFileCount = null)
public static LoggerConfiguration OutputDefaultJsonFile(this LoggerConfiguration logConfig, ILoggingConfiguration loggingConfiguration, LogEventLevel minimumLevel = LogEventLevel.Verbose, int? retainedFileCount = null)
{
//.clef format (Compact log event format, that can be imported into local SEQ & will make searching/filtering logs easier)
//Ends with ..txt as Date is inserted before file extension substring
logConfig.WriteTo.File(new CompactJsonFormatter(), $@"{AppDomain.CurrentDomain.BaseDirectory}\App_Data\Logs\UmbracoTraceLog.{Environment.MachineName}..json",
logConfig.WriteTo.File(new CompactJsonFormatter(), Path.Combine(loggingConfiguration.LogDirectory, $@"UmbracoTraceLog.{Environment.MachineName}..json"),
shared: true,
rollingInterval: RollingInterval.Day, //Create a new JSON file every day
retainedFileCountLimit: retainedFileCount, //Setting to null means we keep all files - default is 31 days
@@ -129,10 +133,11 @@ namespace Umbraco.Core.Logging.Serilog
/// That allows the main logging pipeline to be configured
/// </summary>
/// <param name="logConfig">A Serilog LoggerConfiguration</param>
public static LoggerConfiguration ReadFromConfigFile(this LoggerConfiguration logConfig)
/// <param name="loggingConfiguration"></param>
public static LoggerConfiguration ReadFromConfigFile(this LoggerConfiguration logConfig, ILoggingConfiguration loggingConfiguration)
{
//Read from main serilog.config file
logConfig.ReadFrom.AppSettings(filePath: AppDomain.CurrentDomain.BaseDirectory + @"\config\serilog.config");
logConfig.ReadFrom.AppSettings(filePath: loggingConfiguration.LogConfigurationFile);
return logConfig;
}
@@ -142,13 +147,15 @@ namespace Umbraco.Core.Logging.Serilog
/// That allows a separate logging pipeline to be configured that will not affect the main Umbraco log
/// </summary>
/// <param name="logConfig">A Serilog LoggerConfiguration</param>
public static LoggerConfiguration ReadFromUserConfigFile(this LoggerConfiguration logConfig)
/// <param name="loggingConfiguration"></param>
public static LoggerConfiguration ReadFromUserConfigFile(this LoggerConfiguration logConfig, ILoggingConfiguration loggingConfiguration)
{
//A nested logger - where any user configured sinks via config can not effect the main 'umbraco' logger above
logConfig.WriteTo.Logger(cfg =>
cfg.ReadFrom.AppSettings(filePath: AppDomain.CurrentDomain.BaseDirectory + @"\config\serilog.user.config"));
cfg.ReadFrom.AppSettings(filePath: loggingConfiguration.UserLogConfigurationFile));
return logConfig;
}
}
}
@@ -0,0 +1,21 @@
using System;
using System.Collections.Generic;
using System.Text;
using Umbraco.Core;
using Umbraco.Core.Composing;
using Umbraco.Core.Logging.Serilog.Enrichers;
using Umbraco.Infrastructure.Logging.Serilog.Enrichers;
namespace Umbraco.Infrastructure.Logging.Serilog
{
public class SerilogComposer : ICoreComposer
{
public void Compose(Composition composition)
{
composition.RegisterUnique<ThreadAbortExceptionEnricher>();
composition.RegisterUnique<HttpSessionIdEnricher>();
composition.RegisterUnique<HttpRequestNumberEnricher>();
composition.RegisterUnique<HttpRequestIdEnricher>();
}
}
}
@@ -1,72 +1,56 @@
using System;
using System.IO;
using System.Reflection;
using System.Threading;
using Serilog;
using Serilog.Events;
using Umbraco.Core.Cache;
using Umbraco.Core.Configuration;
using Umbraco.Core.Diagnostics;
using Umbraco.Core.Hosting;
using Umbraco.Core.IO;
using Umbraco.Net;
namespace Umbraco.Core.Logging.Serilog
{
///<summary>
/// Implements <see cref="ILogger"/> on top of Serilog.
///</summary>
public class SerilogLogger : ILogger, IDisposable
{
private readonly ICoreDebugSettings _coreDebugSettings;
private readonly IIOHelper _ioHelper;
private readonly IMarchal _marchal;
public global::Serilog.ILogger SerilogLog { get; }
/// <summary>
/// Initialize a new instance of the <see cref="SerilogLogger"/> class with a configuration file.
/// </summary>
/// <param name="logConfigFile"></param>
public SerilogLogger(ICoreDebugSettings coreDebugSettings, IIOHelper ioHelper, IMarchal marchal, FileInfo logConfigFile)
public SerilogLogger(FileInfo logConfigFile)
{
_coreDebugSettings = coreDebugSettings;
_ioHelper = ioHelper;
_marchal = marchal;
Log.Logger = new LoggerConfiguration()
.ReadFrom.AppSettings(filePath: AppDomain.CurrentDomain.BaseDirectory + logConfigFile)
SerilogLog = new LoggerConfiguration()
.ReadFrom.AppSettings(filePath: logConfigFile.FullName)
.CreateLogger();
}
public SerilogLogger(ICoreDebugSettings coreDebugSettings, IIOHelper ioHelper, IMarchal marchal, LoggerConfiguration logConfig)
public SerilogLogger(LoggerConfiguration logConfig)
{
_coreDebugSettings = coreDebugSettings;
_ioHelper = ioHelper;
_marchal = marchal;
//Configure Serilog static global logger with config passed in
Log.Logger = logConfig.CreateLogger();
SerilogLog = logConfig.CreateLogger();
}
/// <summary>
/// Creates a logger with some pre-defined configuration and remainder from config file
/// </summary>
/// <remarks>Used by UmbracoApplicationBase to get its logger.</remarks>
public static SerilogLogger CreateWithDefaultConfiguration(IHostingEnvironment hostingEnvironment, ISessionIdResolver sessionIdResolver, Func<IRequestCache> requestCacheGetter, ICoreDebugSettings coreDebugSettings, IIOHelper ioHelper, IMarchal marchal)
public static SerilogLogger CreateWithDefaultConfiguration(IHostingEnvironment hostingEnvironment, ILoggingConfiguration loggingConfiguration)
{
var loggerConfig = new LoggerConfiguration();
loggerConfig
.MinimalConfiguration(hostingEnvironment, sessionIdResolver, requestCacheGetter)
.ReadFromConfigFile()
.ReadFromUserConfigFile();
.MinimalConfiguration(hostingEnvironment, loggingConfiguration)
.ReadFromConfigFile(loggingConfiguration)
.ReadFromUserConfigFile(loggingConfiguration);
return new SerilogLogger(coreDebugSettings, ioHelper, marchal, loggerConfig);
return new SerilogLogger(loggerConfig);
}
/// <summary>
/// Gets a contextualized logger.
/// </summary>
private global::Serilog.ILogger LoggerFor(Type reporting)
=> Log.Logger.ForContext(reporting);
=> SerilogLog.ForContext(reporting);
/// <summary>
/// Maps Umbraco's log level to Serilog's.
@@ -99,8 +83,7 @@ namespace Umbraco.Core.Logging.Serilog
/// <inheritdoc/>
public void Fatal(Type reporting, Exception exception, string message)
{
var logger = LoggerFor(reporting);
DumpThreadAborts(logger, LogEventLevel.Fatal, exception, ref message);
var logger = LoggerFor(reporting);
logger.Fatal(exception, message);
}
@@ -108,8 +91,7 @@ namespace Umbraco.Core.Logging.Serilog
public void Fatal(Type reporting, Exception exception)
{
var logger = LoggerFor(reporting);
var message = "Exception.";
DumpThreadAborts(logger, LogEventLevel.Fatal, exception, ref message);
var message = "Exception.";
logger.Fatal(exception, message);
}
@@ -128,16 +110,14 @@ namespace Umbraco.Core.Logging.Serilog
/// <inheritdoc/>
public void Fatal(Type reporting, Exception exception, string messageTemplate, params object[] propertyValues)
{
var logger = LoggerFor(reporting);
DumpThreadAborts(logger, LogEventLevel.Fatal, exception, ref messageTemplate);
var logger = LoggerFor(reporting);
logger.Fatal(exception, messageTemplate, propertyValues);
}
/// <inheritdoc/>
public void Error(Type reporting, Exception exception, string message)
{
var logger = LoggerFor(reporting);
DumpThreadAborts(logger, LogEventLevel.Error, exception, ref message);
var logger = LoggerFor(reporting);
logger.Error(exception, message);
}
@@ -146,7 +126,6 @@ namespace Umbraco.Core.Logging.Serilog
{
var logger = LoggerFor(reporting);
var message = "Exception";
DumpThreadAborts(logger, LogEventLevel.Error, exception, ref message);
logger.Error(exception, message);
}
@@ -166,67 +145,9 @@ namespace Umbraco.Core.Logging.Serilog
public void Error(Type reporting, Exception exception, string messageTemplate, params object[] propertyValues)
{
var logger = LoggerFor(reporting);
DumpThreadAborts(logger, LogEventLevel.Error, exception, ref messageTemplate);
logger.Error(exception, messageTemplate, propertyValues);
}
private void DumpThreadAborts(global::Serilog.ILogger logger, LogEventLevel level, Exception exception, ref string messageTemplate)
{
var dump = false;
if (IsTimeoutThreadAbortException(exception))
{
messageTemplate += "\r\nThe thread has been aborted, because the request has timed out.";
// dump if configured, or if stacktrace contains Monitor.ReliableEnter
dump = _coreDebugSettings.DumpOnTimeoutThreadAbort || IsMonitorEnterThreadAbortException(exception);
// dump if it is ok to dump (might have a cap on number of dump...)
dump &= MiniDump.OkToDump(_ioHelper);
}
if (dump)
{
try
{
var dumped = MiniDump.Dump(_marchal, _ioHelper, withException: true);
messageTemplate += dumped
? "\r\nA minidump was created in App_Data/MiniDump"
: "\r\nFailed to create a minidump";
}
catch (Exception ex)
{
messageTemplate += "\r\nFailed to create a minidump";
//Log a new entry (as opposed to appending to same log entry)
logger.Write(level, ex, "Failed to create a minidump ({ExType}: {ExMessage})",
new object[]{ ex.GetType().FullName, ex.Message });
}
}
}
private static bool IsMonitorEnterThreadAbortException(Exception exception)
{
if (!(exception is ThreadAbortException abort)) return false;
var stacktrace = abort.StackTrace;
return stacktrace.Contains("System.Threading.Monitor.ReliableEnter");
}
private static bool IsTimeoutThreadAbortException(Exception exception)
{
if (!(exception is ThreadAbortException abort)) return false;
if (abort.ExceptionState == null) return false;
var stateType = abort.ExceptionState.GetType();
if (stateType.FullName != "System.Web.HttpApplication+CancelModuleException") return false;
var timeoutField = stateType.GetField("_timeout", BindingFlags.Instance | BindingFlags.NonPublic);
if (timeoutField == null) return false;
return (bool) timeoutField.GetValue(abort.ExceptionState);
}
/// <inheritdoc/>
public void Warn(Type reporting, string message)
{
@@ -289,7 +210,7 @@ namespace Umbraco.Core.Logging.Serilog
public void Dispose()
{
Log.CloseAndFlush();
SerilogLog.DisposeIfDisposable();
}
}
}
@@ -1,7 +1,5 @@
using System;
using System.Collections.Generic;
using System.Collections.Generic;
using Umbraco.Core.Models;
using Umbraco.Core.Persistence.DatabaseModelDefinitions;
namespace Umbraco.Core.Logging.Viewer
{
@@ -0,0 +1,12 @@
using System;
using System.Collections.Generic;
namespace Umbraco.Core.Logging.Viewer
{
public interface ILogViewerConfig
{
IReadOnlyList<SavedLogSearch> GetSavedSearches();
IReadOnlyList<SavedLogSearch> AddSavedSearch(string name, string query);
IReadOnlyList<SavedLogSearch> DeleteSavedSearch(string name, string query);
}
}
@@ -9,7 +9,8 @@ namespace Umbraco.Core.Logging.Viewer
{
public void Compose(Composition composition)
{
composition.SetLogViewer(factory => new JsonLogViewer(composition.Logger, factory.GetInstance<IIOHelper>()));
composition.RegisterUnique<ILogViewerConfig, LogViewerConfig>();
composition.SetLogViewer<SerilogJsonLogViewer>();
}
}
}
@@ -0,0 +1,84 @@
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Newtonsoft.Json;
using Umbraco.Core.Hosting;
using Formatting = Newtonsoft.Json.Formatting;
namespace Umbraco.Core.Logging.Viewer
{
public class LogViewerConfig : ILogViewerConfig
{
private readonly IHostingEnvironment _hostingEnvironment;
private const string _pathToSearches = "~/Config/logviewer.searches.config.js";
private readonly FileInfo _searchesConfig;
public LogViewerConfig(IHostingEnvironment hostingEnvironment)
{
_hostingEnvironment = hostingEnvironment;
var trimmedPath = _pathToSearches.TrimStart('~', '/').Replace('/', Path.DirectorySeparatorChar);
var absolutePath = Path.Combine(_hostingEnvironment.ApplicationPhysicalPath, trimmedPath);
_searchesConfig = new FileInfo(absolutePath);
}
public IReadOnlyList<SavedLogSearch> GetSavedSearches()
{
//Our default implementation
//If file does not exist - lets create it with an empty array
EnsureFileExists();
var rawJson = System.IO.File.ReadAllText(_searchesConfig.FullName);
return JsonConvert.DeserializeObject<SavedLogSearch[]>(rawJson);
}
public IReadOnlyList<SavedLogSearch> AddSavedSearch(string name, string query)
{
//Get the existing items
var searches = GetSavedSearches().ToList();
//Add the new item to the bottom of the list
searches.Add(new SavedLogSearch { Name = name, Query = query });
//Serialize to JSON string
var rawJson = JsonConvert.SerializeObject(searches, Formatting.Indented);
//If file does not exist - lets create it with an empty array
EnsureFileExists();
//Write it back down to file
System.IO.File.WriteAllText(_searchesConfig.FullName, rawJson);
//Return the updated object - so we can instantly reset the entire array from the API response
//As opposed to push a new item into the array
return searches;
}
public IReadOnlyList<SavedLogSearch> DeleteSavedSearch(string name, string query)
{
//Get the existing items
var searches = GetSavedSearches().ToList();
//Removes the search
searches.RemoveAll(s => s.Name.Equals(name) && s.Query.Equals(query));
//Serialize to JSON string
var rawJson = JsonConvert.SerializeObject(searches, Formatting.Indented);
//Write it back down to file
System.IO.File.WriteAllText(_searchesConfig.FullName, rawJson);
//Return the updated object - so we can instantly reset the entire array from the API response
return searches;
}
private void EnsureFileExists()
{
if (_searchesConfig.Exists) return;
using (var writer = _searchesConfig.CreateText())
{
writer.Write("[]");
}
}
}
}
@@ -5,22 +5,25 @@ using System.Linq;
using Newtonsoft.Json;
using Serilog.Events;
using Serilog.Formatting.Compact.Reader;
using Umbraco.Core.Hosting;
using Umbraco.Core.IO;
namespace Umbraco.Core.Logging.Viewer
{
internal class JsonLogViewer : LogViewerSourceBase
internal class SerilogJsonLogViewer : SerilogLogViewerSourceBase
{
private readonly string _logsPath;
private readonly ILogger _logger;
public JsonLogViewer(ILogger logger, IIOHelper ioHelper, string logsPath = "", string searchPath = "") : base(ioHelper, searchPath)
public SerilogJsonLogViewer(
ILogger logger,
ILogViewerConfig logViewerConfig,
ILoggingConfiguration loggingConfiguration,
global::Serilog.ILogger serilogLog)
: base(logViewerConfig, serilogLog)
{
if (string.IsNullOrEmpty(logsPath))
logsPath = $@"{AppDomain.CurrentDomain.BaseDirectory}\App_Data\Logs\";
_logsPath = logsPath;
_logger = logger;
_logsPath = loggingConfiguration.LogDirectory;
}
private const int FileSizeCap = 100;
@@ -62,9 +65,6 @@ namespace Umbraco.Core.Logging.Viewer
{
var logs = new List<LogEvent>();
//Log Directory
var logDirectory = $@"{AppDomain.CurrentDomain.BaseDirectory}\App_Data\Logs\";
var count = 0;
//foreach full day in the range - see if we can find one or more filenames that end with
@@ -74,7 +74,7 @@ namespace Umbraco.Core.Logging.Viewer
//Filename ending to search for (As could be multiple)
var filesToFind = GetSearchPattern(day);
var filesForCurrentDay = Directory.GetFiles(logDirectory, filesToFind);
var filesForCurrentDay = Directory.GetFiles(_logsPath, filesToFind);
//Foreach file we find - open it
foreach (var filePath in filesForCurrentDay)
@@ -130,7 +130,7 @@ namespace Umbraco.Core.Logging.Viewer
{
// As we are reading/streaming one line at a time in the JSON file
// Thus we can not report the line number, as it will always be 1
_logger.Error<JsonLogViewer>(ex, "Unable to parse a line in the JSON log file");
_logger.Error<SerilogJsonLogViewer>(ex, "Unable to parse a line in the JSON log file");
evt = null;
return true;
@@ -1,31 +1,22 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml;
using Newtonsoft.Json;
using Serilog;
using Serilog.Events;
using Umbraco.Core.Composing;
using Umbraco.Core.IO;
using Umbraco.Core.Models;
using Umbraco.Core.Persistence.DatabaseModelDefinitions;
using Formatting = Newtonsoft.Json.Formatting;
namespace Umbraco.Core.Logging.Viewer
{
public abstract class LogViewerSourceBase : ILogViewer
public abstract class SerilogLogViewerSourceBase : ILogViewer
{
private readonly string _searchesConfigPath;
private readonly IIOHelper _ioHelper;
private readonly ILogViewerConfig _logViewerConfig;
private readonly global::Serilog.ILogger _serilogLog;
protected LogViewerSourceBase(IIOHelper ioHelper, string pathToSearches = "")
{
if (string.IsNullOrEmpty(pathToSearches))
// ReSharper disable once StringLiteralTypo
pathToSearches = ioHelper.MapPath("~/Config/logviewer.searches.config.js");
_searchesConfigPath = pathToSearches;
_ioHelper = ioHelper;
protected SerilogLogViewerSourceBase(ILogViewerConfig logViewerConfig, global::Serilog.ILogger serilogLog)
{
_logViewerConfig = logViewerConfig;
_serilogLog = serilogLog;
}
public abstract bool CanHandleLargeLogs { get; }
@@ -38,55 +29,13 @@ namespace Umbraco.Core.Logging.Viewer
public abstract bool CheckCanOpenLogs(LogTimePeriod logTimePeriod);
public virtual IReadOnlyList<SavedLogSearch> GetSavedSearches()
{
//Our default implementation
//If file does not exist - lets create it with an empty array
EnsureFileExists(_searchesConfigPath, "[]", _ioHelper);
var rawJson = System.IO.File.ReadAllText(_searchesConfigPath);
return JsonConvert.DeserializeObject<SavedLogSearch[]>(rawJson);
}
=> _logViewerConfig.GetSavedSearches();
public virtual IReadOnlyList<SavedLogSearch> AddSavedSearch(string name, string query)
{
//Get the existing items
var searches = GetSavedSearches().ToList();
//Add the new item to the bottom of the list
searches.Add(new SavedLogSearch { Name = name, Query = query });
//Serialize to JSON string
var rawJson = JsonConvert.SerializeObject(searches, Formatting.Indented);
//If file does not exist - lets create it with an empty array
EnsureFileExists(_searchesConfigPath, "[]", _ioHelper);
//Write it back down to file
System.IO.File.WriteAllText(_searchesConfigPath, rawJson);
//Return the updated object - so we can instantly reset the entire array from the API response
//As opposed to push a new item into the array
return searches;
}
=> _logViewerConfig.AddSavedSearch(name, query);
public virtual IReadOnlyList<SavedLogSearch> DeleteSavedSearch(string name, string query)
{
//Get the existing items
var searches = GetSavedSearches().ToList();
//Removes the search
searches.RemoveAll(s => s.Name.Equals(name) && s.Query.Equals(query));
//Serialize to JSON string
var rawJson = JsonConvert.SerializeObject(searches, Formatting.Indented);
//Write it back down to file
System.IO.File.WriteAllText(_searchesConfigPath, rawJson);
//Return the updated object - so we can instantly reset the entire array from the API response
return searches;
}
=> _logViewerConfig.DeleteSavedSearch(name, query);
public int GetNumberOfErrors(LogTimePeriod logTimePeriod)
{
@@ -101,7 +50,7 @@ namespace Umbraco.Core.Logging.Viewer
/// <returns></returns>
public string GetLogLevel()
{
var logLevel = Enum.GetValues(typeof(LogEventLevel)).Cast<LogEventLevel>().Where(Log.Logger.IsEnabled)?.Min() ?? null;
var logLevel = Enum.GetValues(typeof(LogEventLevel)).Cast<LogEventLevel>().Where(_serilogLog.IsEnabled)?.Min() ?? null;
return logLevel?.ToString() ?? "";
}
@@ -182,15 +131,6 @@ namespace Umbraco.Core.Logging.Viewer
};
}
private static void EnsureFileExists(string path, string contents, IIOHelper ioHelper)
{
var absolutePath = ioHelper.MapPath(path);
if (System.IO.File.Exists(absolutePath)) return;
using (var writer = System.IO.File.CreateText(absolutePath))
{
writer.Write(contents);
}
}
}
}
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using Umbraco.Core.Models;
using Umbraco.Core.Models.Entities;
using Umbraco.Core.Persistence.DatabaseModelDefinitions;
using Umbraco.Core.Persistence.Querying;
@@ -77,5 +78,7 @@ namespace Umbraco.Core.Persistence.Repositories
/// <remarks>Here, <paramref name="filter"/> can be null but <paramref name="ordering"/> cannot.</remarks>
IEnumerable<TEntity> GetPage(IQuery<TEntity> query, long pageIndex, int pageSize, out long totalRecords,
IQuery<TEntity> filter, Ordering ordering);
ContentDataIntegrityReport CheckDataIntegrity(ContentDataIntegrityReportOptions options);
}
}
@@ -91,7 +91,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
// gets all version ids, current first
public virtual IEnumerable<int> GetVersionIds(int nodeId, int maxRows)
{
var template = SqlContext.Templates.Get("Umbraco.Core.VersionableRepository.GetVersionIds", tsql =>
var template = SqlContext.Templates.Get(Constants.SqlTemplates.VersionableRepository.GetVersionIds, tsql =>
tsql.Select<ContentVersionDto>(x => x.Id)
.From<ContentVersionDto>()
.Where<ContentVersionDto>(x => x.NodeId == SqlTemplate.Arg<int>("nodeId"))
@@ -107,7 +107,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
// TODO: test object node type?
// get the version we want to delete
var template = SqlContext.Templates.Get("Umbraco.Core.VersionableRepository.GetVersion", tsql =>
var template = SqlContext.Templates.Get(Constants.SqlTemplates.VersionableRepository.GetVersion, tsql =>
tsql.Select<ContentVersionDto>().From<ContentVersionDto>().Where<ContentVersionDto>(x => x.Id == SqlTemplate.Arg<int>("versionId"))
);
var versionDto = Database.Fetch<ContentVersionDto>(template.Sql(new { versionId })).FirstOrDefault();
@@ -129,7 +129,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
// TODO: test object node type?
// get the versions we want to delete, excluding the current one
var template = SqlContext.Templates.Get("Umbraco.Core.VersionableRepository.GetVersions", tsql =>
var template = SqlContext.Templates.Get(Constants.SqlTemplates.VersionableRepository.GetVersions, tsql =>
tsql.Select<ContentVersionDto>().From<ContentVersionDto>().Where<ContentVersionDto>(x => x.NodeId == SqlTemplate.Arg<int>("nodeId") && !x.Current && x.VersionDate < SqlTemplate.Arg<DateTime>("versionDate"))
);
var versionDtos = Database.Fetch<ContentVersionDto>(template.Sql(new { nodeId, versionDate }));
@@ -411,7 +411,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
}
// content type alias is invariant
if(ordering.OrderBy.InvariantEquals("contentTypeAlias"))
if (ordering.OrderBy.InvariantEquals("contentTypeAlias"))
{
var joins = Sql()
.InnerJoin<ContentTypeDto>("ctype").On<ContentDto, ContentTypeDto>((content, contentType) => content.ContentTypeId == contentType.NodeId, aliasRight: "ctype");
@@ -485,6 +485,123 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
IQuery<TEntity> filter,
Ordering ordering);
public ContentDataIntegrityReport CheckDataIntegrity(ContentDataIntegrityReportOptions options)
{
var report = new Dictionary<int, ContentDataIntegrityReportEntry>();
var sql = SqlContext.Sql()
.Select<NodeDto>()
.From<NodeDto>()
.Where<NodeDto>(x => x.NodeObjectType == NodeObjectTypeId)
.OrderBy<NodeDto>(x => x.Level, x => x.ParentId, x => x.SortOrder);
var nodesToRebuild = new Dictionary<int, List<NodeDto>>();
var validNodes = new Dictionary<int, NodeDto>();
var rootIds = new[] {Constants.System.Root, Constants.System.RecycleBinContent, Constants.System.RecycleBinMedia};
var currentParentIds = new HashSet<int>(rootIds);
var prevParentIds = currentParentIds;
var lastLevel = -1;
// use a forward cursor (query)
foreach (var node in Database.Query<NodeDto>(sql))
{
if (node.Level != lastLevel)
{
// changing levels
prevParentIds = currentParentIds;
currentParentIds = null;
lastLevel = node.Level;
}
if (currentParentIds == null)
{
// we're reset
currentParentIds = new HashSet<int>();
}
currentParentIds.Add(node.NodeId);
// paths parts without the roots
var pathParts = node.Path.Split(',').Where(x => !rootIds.Contains(int.Parse(x))).ToArray();
if (!prevParentIds.Contains(node.ParentId))
{
// invalid, this will be because the level is wrong (which prob means path is wrong too)
report.Add(node.NodeId, new ContentDataIntegrityReportEntry(ContentDataIntegrityReport.IssueType.InvalidPathAndLevelByParentId));
AppendNodeToFix(nodesToRebuild, node);
}
else if (pathParts.Length == 0)
{
// invalid path
report.Add(node.NodeId, new ContentDataIntegrityReportEntry(ContentDataIntegrityReport.IssueType.InvalidPathEmpty));
AppendNodeToFix(nodesToRebuild, node);
}
else if (pathParts.Length != node.Level)
{
// invalid, either path or level is wrong
report.Add(node.NodeId, new ContentDataIntegrityReportEntry(ContentDataIntegrityReport.IssueType.InvalidPathLevelMismatch));
AppendNodeToFix(nodesToRebuild, node);
}
else if (pathParts[pathParts.Length - 1] != node.NodeId.ToString())
{
// invalid path
report.Add(node.NodeId, new ContentDataIntegrityReportEntry(ContentDataIntegrityReport.IssueType.InvalidPathById));
AppendNodeToFix(nodesToRebuild, node);
}
else if (!rootIds.Contains(node.ParentId) && pathParts[pathParts.Length - 2] != node.ParentId.ToString())
{
// invalid path
report.Add(node.NodeId, new ContentDataIntegrityReportEntry(ContentDataIntegrityReport.IssueType.InvalidPathByParentId));
AppendNodeToFix(nodesToRebuild, node);
}
else
{
// it's valid!
// don't track unless we are configured to fix
if (options.FixIssues)
validNodes.Add(node.NodeId, node);
}
}
var updated = new List<NodeDto>();
if (options.FixIssues)
{
// iterate all valid nodes to see if these are parents for invalid nodes
foreach (var (nodeId, node) in validNodes)
{
if (!nodesToRebuild.TryGetValue(nodeId, out var invalidNodes)) continue;
// now we can try to rebuild the invalid paths.
foreach (var invalidNode in invalidNodes)
{
invalidNode.Level = (short)(node.Level + 1);
invalidNode.Path = node.Path + "," + invalidNode.NodeId;
updated.Add(invalidNode);
}
}
foreach (var node in updated)
{
Database.Update(node);
if (report.TryGetValue(node.NodeId, out var entry))
entry.Fixed = true;
}
}
return new ContentDataIntegrityReport(report);
}
private static void AppendNodeToFix(IDictionary<int, List<NodeDto>> nodesToRebuild, NodeDto node)
{
if (nodesToRebuild.TryGetValue(node.ParentId, out var childIds))
childIds.Add(node);
else
nodesToRebuild[node.ParentId] = new List<NodeDto> { node };
}
// here, filter can be null and ordering cannot
protected IEnumerable<TEntity> GetPage<TDto>(IQuery<TEntity> query,
long pageIndex, int pageSize, out long totalRecords,
@@ -778,7 +895,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
protected virtual string EnsureUniqueNodeName(int parentId, string nodeName, int id = 0)
{
var template = SqlContext.Templates.Get("Umbraco.Core.VersionableRepository.EnsureUniqueNodeName", tsql => tsql
var template = SqlContext.Templates.Get(Constants.SqlTemplates.VersionableRepository.EnsureUniqueNodeName, tsql => tsql
.Select<NodeDto>(x => Alias(x.NodeId, "id"), x => Alias(x.Text, "name"))
.From<NodeDto>()
.Where<NodeDto>(x => x.NodeObjectType == SqlTemplate.Arg<Guid>("nodeObjectType") && x.ParentId == SqlTemplate.Arg<int>("parentId")));
@@ -791,7 +908,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
protected virtual int GetNewChildSortOrder(int parentId, int first)
{
var template = SqlContext.Templates.Get("Umbraco.Core.VersionableRepository.GetSortOrder", tsql =>
var template = SqlContext.Templates.Get(Constants.SqlTemplates.VersionableRepository.GetSortOrder, tsql =>
tsql.Select($"COALESCE(MAX(sortOrder),{first - 1})").From<NodeDto>().Where<NodeDto>(x => x.ParentId == SqlTemplate.Arg<int>("parentId") && x.NodeObjectType == NodeObjectTypeId)
);
@@ -800,7 +917,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
protected virtual NodeDto GetParentNodeDto(int parentId)
{
var template = SqlContext.Templates.Get("Umbraco.Core.VersionableRepository.GetParentNode", tsql =>
var template = SqlContext.Templates.Get(Constants.SqlTemplates.VersionableRepository.GetParentNode, tsql =>
tsql.Select<NodeDto>().From<NodeDto>().Where<NodeDto>(x => x.NodeId == SqlTemplate.Arg<int>("parentId"))
);
@@ -809,7 +926,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
protected virtual int GetReservedId(Guid uniqueId)
{
var template = SqlContext.Templates.Get("Umbraco.Core.VersionableRepository.GetReservedId", tsql =>
var template = SqlContext.Templates.Get(Constants.SqlTemplates.VersionableRepository.GetReservedId, tsql =>
tsql.Select<NodeDto>(x => x.NodeId).From<NodeDto>().Where<NodeDto>(x => x.UniqueId == SqlTemplate.Arg<Guid>("uniqueId") && x.NodeObjectType == Constants.ObjectTypes.IdReservation)
);
var id = Database.ExecuteScalar<int?>(template.Sql(new { uniqueId = uniqueId }));
@@ -331,7 +331,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
.InnerJoin<DocumentVersionDto>()
.On<ContentVersionDto, DocumentVersionDto>((c, d) => c.Id == d.Id)
.Where<ContentVersionDto>(x => x.NodeId == SqlTemplate.Arg<int>("nodeId") && !x.Current && x.VersionDate < SqlTemplate.Arg<DateTime>("versionDate"))
.Where<DocumentVersionDto>( x => !x.Published)
.Where<DocumentVersionDto>(x => !x.Published)
);
var versionDtos = Database.Fetch<ContentVersionDto>(template.Sql(new { nodeId, versionDate }));
foreach (var versionDto in versionDtos)
@@ -529,8 +529,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
protected override void PersistUpdatedItem(IContent entity)
{
var entityBase = entity as EntityBase;
var isEntityDirty = entityBase != null && entityBase.IsDirty();
var isEntityDirty = entity.IsDirty();
// check if we need to make any database changes at all
if ((entity.PublishedState == PublishedState.Published || entity.PublishedState == PublishedState.Unpublished)
@@ -545,29 +544,41 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
// update
entity.UpdatingEntity();
// Check if this entity is being moved as a descendant as part of a bulk moving operations.
// In this case we can bypass a lot of the below operations which will make this whole operation go much faster.
// When moving we don't need to create new versions, etc... because we cannot roll this operation back anyways.
var isMoving = entity.IsMoving();
// TODO: I'm sure we can also detect a "Copy" (of a descendant) operation and probably perform similar checks below.
// There is probably more stuff that would be required for copying but I'm sure not all of this logic would be, we could more than likely boost
// copy performance by 95% just like we did for Move
var publishing = entity.PublishedState == PublishedState.Publishing;
// check if we need to create a new version
if (publishing && entity.PublishedVersionId > 0)
if (!isMoving)
{
// published version is not published anymore
Database.Execute(Sql().Update<DocumentVersionDto>(u => u.Set(x => x.Published, false)).Where<DocumentVersionDto>(x => x.Id == entity.PublishedVersionId));
}
// check if we need to create a new version
if (publishing && entity.PublishedVersionId > 0)
{
// published version is not published anymore
Database.Execute(Sql().Update<DocumentVersionDto>(u => u.Set(x => x.Published, false)).Where<DocumentVersionDto>(x => x.Id == entity.PublishedVersionId));
}
// sanitize names
SanitizeNames(entity, publishing);
// sanitize names
SanitizeNames(entity, publishing);
// ensure that strings don't contain characters that are invalid in xml
// TODO: do we really want to keep doing this here?
entity.SanitizeEntityPropertiesForXmlStorage();
// ensure that strings don't contain characters that are invalid in xml
// TODO: do we really want to keep doing this here?
entity.SanitizeEntityPropertiesForXmlStorage();
// if parent has changed, get path, level and sort order
if (entity.IsPropertyDirty("ParentId"))
{
var parent = GetParentNodeDto(entity.ParentId);
entity.Path = string.Concat(parent.Path, ",", entity.Id);
entity.Level = parent.Level + 1;
entity.SortOrder = GetNewChildSortOrder(entity.ParentId, 0);
// if parent has changed, get path, level and sort order
if (entity.IsPropertyDirty("ParentId"))
{
var parent = GetParentNodeDto(entity.ParentId);
entity.Path = string.Concat(parent.Path, ",", entity.Id);
entity.Level = parent.Level + 1;
entity.SortOrder = GetNewChildSortOrder(entity.ParentId, 0);
}
}
// create the dto
@@ -578,146 +589,152 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
nodeDto.ValidatePathWithException();
Database.Update(nodeDto);
// update the content dto
Database.Update(dto.ContentDto);
// update the content & document version dtos
var contentVersionDto = dto.DocumentVersionDto.ContentVersionDto;
var documentVersionDto = dto.DocumentVersionDto;
if (publishing)
if (!isMoving)
{
documentVersionDto.Published = true; // now published
contentVersionDto.Current = false; // no more current
}
Database.Update(contentVersionDto);
Database.Update(documentVersionDto);
// update the content dto
Database.Update(dto.ContentDto);
// and, if publishing, insert new content & document version dtos
if (publishing)
{
entity.PublishedVersionId = entity.VersionId;
contentVersionDto.Id = 0; // want a new id
contentVersionDto.Current = true; // current version
contentVersionDto.Text = entity.Name;
Database.Insert(contentVersionDto);
entity.VersionId = documentVersionDto.Id = contentVersionDto.Id; // get the new id
documentVersionDto.Published = false; // non-published version
Database.Insert(documentVersionDto);
}
// replace the property data (rather than updating)
// only need to delete for the version that existed, the new version (if any) has no property data yet
var versionToDelete = publishing ? entity.PublishedVersionId : entity.VersionId;
var deletePropertyDataSql = Sql().Delete<PropertyDataDto>().Where<PropertyDataDto>(x => x.VersionId == versionToDelete);
Database.Execute(deletePropertyDataSql);
// insert property data
var propertyDataDtos = PropertyFactory.BuildDtos(entity.ContentType.Variations, entity.VersionId, publishing ? entity.PublishedVersionId : 0,
entity.Properties, LanguageRepository, out var edited, out var editedCultures);
foreach (var propertyDataDto in propertyDataDtos)
Database.Insert(propertyDataDto);
// if !publishing, we may have a new name != current publish name,
// also impacts 'edited'
if (!publishing && entity.PublishName != entity.Name)
edited = true;
if (entity.ContentType.VariesByCulture())
{
// bump dates to align cultures to version
// update the content & document version dtos
var contentVersionDto = dto.DocumentVersionDto.ContentVersionDto;
var documentVersionDto = dto.DocumentVersionDto;
if (publishing)
entity.AdjustDates(contentVersionDto.VersionDate);
{
documentVersionDto.Published = true; // now published
contentVersionDto.Current = false; // no more current
}
Database.Update(contentVersionDto);
Database.Update(documentVersionDto);
// names also impact 'edited'
// ReSharper disable once UseDeconstruction
foreach (var cultureInfo in entity.CultureInfos)
if (cultureInfo.Name != entity.GetPublishName(cultureInfo.Culture))
{
edited = true;
(editedCultures ?? (editedCultures = new HashSet<string>(StringComparer.OrdinalIgnoreCase))).Add(cultureInfo.Culture);
// and, if publishing, insert new content & document version dtos
if (publishing)
{
entity.PublishedVersionId = entity.VersionId;
// TODO: change tracking
// at the moment, we don't do any dirty tracking on property values, so we don't know whether the
// culture has just been edited or not, so we don't update its update date - that date only changes
// when the name is set, and it all works because the controller does it - but, if someone uses a
// service to change a property value and save (without setting name), the update date does not change.
}
contentVersionDto.Id = 0; // want a new id
contentVersionDto.Current = true; // current version
contentVersionDto.Text = entity.Name;
Database.Insert(contentVersionDto);
entity.VersionId = documentVersionDto.Id = contentVersionDto.Id; // get the new id
// replace the content version variations (rather than updating)
documentVersionDto.Published = false; // non-published version
Database.Insert(documentVersionDto);
}
// replace the property data (rather than updating)
// only need to delete for the version that existed, the new version (if any) has no property data yet
var deleteContentVariations = Sql().Delete<ContentVersionCultureVariationDto>().Where<ContentVersionCultureVariationDto>(x => x.VersionId == versionToDelete);
Database.Execute(deleteContentVariations);
var versionToDelete = publishing ? entity.PublishedVersionId : entity.VersionId;
var deletePropertyDataSql = Sql().Delete<PropertyDataDto>().Where<PropertyDataDto>(x => x.VersionId == versionToDelete);
Database.Execute(deletePropertyDataSql);
// replace the document version variations (rather than updating)
var deleteDocumentVariations = Sql().Delete<DocumentCultureVariationDto>().Where<DocumentCultureVariationDto>(x => x.NodeId == entity.Id);
Database.Execute(deleteDocumentVariations);
// insert property data
var propertyDataDtos = PropertyFactory.BuildDtos(entity.ContentType.Variations, entity.VersionId, publishing ? entity.PublishedVersionId : 0,
entity.Properties, LanguageRepository, out var edited, out var editedCultures);
foreach (var propertyDataDto in propertyDataDtos)
Database.Insert(propertyDataDto);
// TODO: NPoco InsertBulk issue?
// we should use the native NPoco InsertBulk here but it causes problems (not sure exactly all scenarios)
// but by using SQL Server and updating a variants name will cause: Unable to cast object of type
// 'Umbraco.Core.Persistence.FaultHandling.RetryDbConnection' to type 'System.Data.SqlClient.SqlConnection'.
// (same in PersistNewItem above)
// if !publishing, we may have a new name != current publish name,
// also impacts 'edited'
if (!publishing && entity.PublishName != entity.Name)
edited = true;
// insert content variations
Database.BulkInsertRecords(GetContentVariationDtos(entity, publishing));
if (entity.ContentType.VariesByCulture())
{
// bump dates to align cultures to version
if (publishing)
entity.AdjustDates(contentVersionDto.VersionDate);
// insert document variations
Database.BulkInsertRecords(GetDocumentVariationDtos(entity, editedCultures));
// names also impact 'edited'
// ReSharper disable once UseDeconstruction
foreach (var cultureInfo in entity.CultureInfos)
if (cultureInfo.Name != entity.GetPublishName(cultureInfo.Culture))
{
edited = true;
(editedCultures ?? (editedCultures = new HashSet<string>(StringComparer.OrdinalIgnoreCase))).Add(cultureInfo.Culture);
// TODO: change tracking
// at the moment, we don't do any dirty tracking on property values, so we don't know whether the
// culture has just been edited or not, so we don't update its update date - that date only changes
// when the name is set, and it all works because the controller does it - but, if someone uses a
// service to change a property value and save (without setting name), the update date does not change.
}
// replace the content version variations (rather than updating)
// only need to delete for the version that existed, the new version (if any) has no property data yet
var deleteContentVariations = Sql().Delete<ContentVersionCultureVariationDto>().Where<ContentVersionCultureVariationDto>(x => x.VersionId == versionToDelete);
Database.Execute(deleteContentVariations);
// replace the document version variations (rather than updating)
var deleteDocumentVariations = Sql().Delete<DocumentCultureVariationDto>().Where<DocumentCultureVariationDto>(x => x.NodeId == entity.Id);
Database.Execute(deleteDocumentVariations);
// TODO: NPoco InsertBulk issue?
// we should use the native NPoco InsertBulk here but it causes problems (not sure exactly all scenarios)
// but by using SQL Server and updating a variants name will cause: Unable to cast object of type
// 'Umbraco.Core.Persistence.FaultHandling.RetryDbConnection' to type 'System.Data.SqlClient.SqlConnection'.
// (same in PersistNewItem above)
// insert content variations
Database.BulkInsertRecords(GetContentVariationDtos(entity, publishing));
// insert document variations
Database.BulkInsertRecords(GetDocumentVariationDtos(entity, editedCultures));
}
// refresh content
entity.SetCultureEdited(editedCultures);
// update the document dto
// at that point, when un/publishing, the entity still has its old Published value
// so we need to explicitly update the dto to persist the correct value
if (entity.PublishedState == PublishedState.Publishing)
dto.Published = true;
else if (entity.PublishedState == PublishedState.Unpublishing)
dto.Published = false;
entity.Edited = dto.Edited = !dto.Published || edited; // if not published, always edited
Database.Update(dto);
//update the schedule
if (entity.IsPropertyDirty("ContentSchedule"))
PersistContentSchedule(entity, true);
// if entity is publishing, update tags, else leave tags there
// means that implicitly unpublished, or trashed, entities *still* have tags in db
if (entity.PublishedState == PublishedState.Publishing)
SetEntityTags(entity, _tagRepository);
}
// refresh content
entity.SetCultureEdited(editedCultures);
// update the document dto
// at that point, when un/publishing, the entity still has its old Published value
// so we need to explicitly update the dto to persist the correct value
if (entity.PublishedState == PublishedState.Publishing)
dto.Published = true;
else if (entity.PublishedState == PublishedState.Unpublishing)
dto.Published = false;
entity.Edited = dto.Edited = !dto.Published || edited; // if not published, always edited
Database.Update(dto);
//update the schedule
if (entity.IsPropertyDirty("ContentSchedule"))
PersistContentSchedule(entity, true);
// if entity is publishing, update tags, else leave tags there
// means that implicitly unpublished, or trashed, entities *still* have tags in db
if (entity.PublishedState == PublishedState.Publishing)
SetEntityTags(entity, _tagRepository);
// trigger here, before we reset Published etc
OnUowRefreshedEntity(new ScopedEntityEventArgs(AmbientScope, entity));
// flip the entity's published property
// this also flips its published state
if (entity.PublishedState == PublishedState.Publishing)
if (!isMoving)
{
entity.Published = true;
entity.PublishTemplateId = entity.TemplateId;
entity.PublisherId = entity.WriterId;
entity.PublishName = entity.Name;
entity.PublishDate = entity.UpdateDate;
// flip the entity's published property
// this also flips its published state
if (entity.PublishedState == PublishedState.Publishing)
{
entity.Published = true;
entity.PublishTemplateId = entity.TemplateId;
entity.PublisherId = entity.WriterId;
entity.PublishName = entity.Name;
entity.PublishDate = entity.UpdateDate;
SetEntityTags(entity, _tagRepository);
SetEntityTags(entity, _tagRepository);
}
else if (entity.PublishedState == PublishedState.Unpublishing)
{
entity.Published = false;
entity.PublishTemplateId = null;
entity.PublisherId = null;
entity.PublishName = null;
entity.PublishDate = null;
ClearEntityTags(entity, _tagRepository);
}
PersistRelations(entity);
// TODO: note re. tags: explicitly unpublished entities have cleared tags, but masked or trashed entities *still* have tags in the db - so what?
}
else if (entity.PublishedState == PublishedState.Unpublishing)
{
entity.Published = false;
entity.PublishTemplateId = null;
entity.PublisherId = null;
entity.PublishName = null;
entity.PublishDate = null;
ClearEntityTags(entity, _tagRepository);
}
PersistRelations(entity);
// TODO: note re. tags: explicitly unpublished entities have cleared tags, but masked or trashed entities *still* have tags in the db - so what?
entity.ResetDirtyProperties();
@@ -231,7 +231,6 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
protected override void PersistNewItem(IMedia entity)
{
var media = (Models.Media) entity;
entity.AddingEntity();
// ensure unique name on the same level
@@ -286,15 +285,15 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
contentVersionDto.NodeId = nodeDto.NodeId;
contentVersionDto.Current = true;
Database.Insert(contentVersionDto);
media.VersionId = contentVersionDto.Id;
entity.VersionId = contentVersionDto.Id;
// persist the media version dto
var mediaVersionDto = dto.MediaVersionDto;
mediaVersionDto.Id = media.VersionId;
mediaVersionDto.Id = entity.VersionId;
Database.Insert(mediaVersionDto);
// persist the property data
var propertyDataDtos = PropertyFactory.BuildDtos(media.ContentType.Variations, media.VersionId, 0, entity.Properties, LanguageRepository, out _, out _);
var propertyDataDtos = PropertyFactory.BuildDtos(entity.ContentType.Variations, entity.VersionId, 0, entity.Properties, LanguageRepository, out _, out _);
foreach (var propertyDataDto in propertyDataDtos)
Database.Insert(propertyDataDto);
@@ -310,26 +309,32 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
protected override void PersistUpdatedItem(IMedia entity)
{
var media = (Models.Media) entity;
// update
media.UpdatingEntity();
entity.UpdatingEntity();
// ensure unique name on the same level
entity.Name = EnsureUniqueNodeName(entity.ParentId, entity.Name, entity.Id);
// Check if this entity is being moved as a descendant as part of a bulk moving operations.
// In this case we can bypass a lot of the below operations which will make this whole operation go much faster.
// When moving we don't need to create new versions, etc... because we cannot roll this operation back anyways.
var isMoving = entity.IsMoving();
// ensure that strings don't contain characters that are invalid in xml
// TODO: do we really want to keep doing this here?
entity.SanitizeEntityPropertiesForXmlStorage();
// if parent has changed, get path, level and sort order
if (entity.IsPropertyDirty("ParentId"))
if (!isMoving)
{
var parent = GetParentNodeDto(entity.ParentId);
// ensure unique name on the same level
entity.Name = EnsureUniqueNodeName(entity.ParentId, entity.Name, entity.Id);
entity.Path = string.Concat(parent.Path, ",", entity.Id);
entity.Level = parent.Level + 1;
entity.SortOrder = GetNewChildSortOrder(entity.ParentId, 0);
// ensure that strings don't contain characters that are invalid in xml
// TODO: do we really want to keep doing this here?
entity.SanitizeEntityPropertiesForXmlStorage();
// if parent has changed, get path, level and sort order
if (entity.IsPropertyDirty(nameof(entity.ParentId)))
{
var parent = GetParentNodeDto(entity.ParentId);
entity.Path = string.Concat(parent.Path, ",", entity.Id);
entity.Level = parent.Level + 1;
entity.SortOrder = GetNewChildSortOrder(entity.ParentId, 0);
}
}
// create the dto
@@ -340,26 +345,29 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
nodeDto.ValidatePathWithException();
Database.Update(nodeDto);
// update the content dto
Database.Update(dto.ContentDto);
if (!isMoving)
{
// update the content dto
Database.Update(dto.ContentDto);
// update the content & media version dtos
var contentVersionDto = dto.MediaVersionDto.ContentVersionDto;
var mediaVersionDto = dto.MediaVersionDto;
contentVersionDto.Current = true;
Database.Update(contentVersionDto);
Database.Update(mediaVersionDto);
// update the content & media version dtos
var contentVersionDto = dto.MediaVersionDto.ContentVersionDto;
var mediaVersionDto = dto.MediaVersionDto;
contentVersionDto.Current = true;
Database.Update(contentVersionDto);
Database.Update(mediaVersionDto);
// replace the property data
var deletePropertyDataSql = SqlContext.Sql().Delete<PropertyDataDto>().Where<PropertyDataDto>(x => x.VersionId == media.VersionId);
Database.Execute(deletePropertyDataSql);
var propertyDataDtos = PropertyFactory.BuildDtos(media.ContentType.Variations, media.VersionId, 0, entity.Properties, LanguageRepository, out _, out _);
foreach (var propertyDataDto in propertyDataDtos)
Database.Insert(propertyDataDto);
// replace the property data
var deletePropertyDataSql = SqlContext.Sql().Delete<PropertyDataDto>().Where<PropertyDataDto>(x => x.VersionId == entity.VersionId);
Database.Execute(deletePropertyDataSql);
var propertyDataDtos = PropertyFactory.BuildDtos(entity.ContentType.Variations, entity.VersionId, 0, entity.Properties, LanguageRepository, out _, out _);
foreach (var propertyDataDto in propertyDataDtos)
Database.Insert(propertyDataDto);
SetEntityTags(entity, _tagRepository);
SetEntityTags(entity, _tagRepository);
PersistRelations(entity);
PersistRelations(entity);
}
OnUowRefreshedEntity(new ScopedEntityEventArgs(AmbientScope, entity));
@@ -29,6 +29,7 @@ using Umbraco.Core.Strings;
using Umbraco.Core.Sync;
using Umbraco.Examine;
using Umbraco.Infrastructure.Media;
using Umbraco.Net;
using Umbraco.Web;
using Umbraco.Web.Actions;
using Umbraco.Web.Cache;
@@ -351,10 +352,6 @@ namespace Umbraco.Core.Runtime
// register accessors for cultures
composition.RegisterUnique<IDefaultCultureAccessor, DefaultCultureAccessor>();
}
}
}
@@ -25,6 +25,7 @@ namespace Umbraco.Core.Runtime
private IFactory _factory;
private readonly RuntimeState _state;
private readonly IUmbracoBootPermissionChecker _umbracoBootPermissionChecker;
private readonly IRequestCache _requestCache;
private readonly IGlobalSettings _globalSettings;
private readonly IConnectionStrings _connectionStrings;
@@ -39,7 +40,8 @@ namespace Umbraco.Core.Runtime
IBackOfficeInfo backOfficeInfo,
IDbProviderFactoryCreator dbProviderFactoryCreator,
IMainDom mainDom,
ITypeFinder typeFinder)
ITypeFinder typeFinder,
IRequestCache requestCache)
{
IOHelper = ioHelper;
Configs = configs;
@@ -50,6 +52,7 @@ namespace Umbraco.Core.Runtime
DbProviderFactoryCreator = dbProviderFactoryCreator;
_umbracoBootPermissionChecker = umbracoBootPermissionChecker;
_requestCache = requestCache;
Logger = logger;
MainDom = mainDom;
@@ -110,6 +113,7 @@ namespace Umbraco.Core.Runtime
{
if (register is null) throw new ArgumentNullException(nameof(register));
// create and register the essential services
// ie the bare minimum required to boot
@@ -129,12 +133,19 @@ namespace Umbraco.Core.Runtime
"Booted.",
"Boot failed."))
{
Logger.Info<CoreRuntime>("Booting Core");
Logger.Info<CoreRuntime>("Booting site '{HostingSiteName}', app '{HostingApplicationId}', path '{HostingPhysicalPath}', server '{MachineName}'.",
HostingEnvironment?.SiteName,
HostingEnvironment?.ApplicationId,
HostingEnvironment?.ApplicationPhysicalPath,
NetworkHelper.MachineName);
Logger.Debug<CoreRuntime>("Runtime: {Runtime}", GetType().FullName);
// application environment
ConfigureUnhandledException();
return _factory = Configure(register, timer);
_factory = Configure(register, timer);
return _factory;
}
}
@@ -151,7 +162,7 @@ namespace Umbraco.Core.Runtime
try
{
// run handlers
RuntimeOptions.DoRuntimeBoot(ProfilingLogger);
@@ -244,6 +255,13 @@ namespace Umbraco.Core.Runtime
// create & initialize the components
_components = _factory.GetInstance<ComponentCollection>();
_components.Initialize();
// now (and only now) is the time to switch over to perWebRequest scopes.
// up until that point we may not have a request, and scoped services would
// fail to resolve - but we run Initialize within a factory scope - and then,
// here, we switch the factory to bind scopes to requests
_factory.EnablePerWebRequestScope();
}
protected virtual void ConfigureUnhandledException()
@@ -350,7 +368,7 @@ namespace Umbraco.Core.Runtime
return new AppCaches(
new DeepCloneAppCache(new ObjectCacheAppCache()),
NoAppCache.Instance,
_requestCache,
new IsolatedCaches(type => new DeepCloneAppCache(new ObjectCacheAppCache())));
}
@@ -1,90 +0,0 @@
using Umbraco.Core;
using Umbraco.Core.Cache;
using Umbraco.Core.Composing;
using Umbraco.Core.Configuration;
using Umbraco.Core.Hosting;
using Umbraco.Core.IO;
using Umbraco.Core.Logging;
using Umbraco.Core.Persistence;
using Umbraco.Core.Runtime;
namespace Umbraco.Web.Runtime
{
/// <summary>
/// Represents the Web Umbraco runtime.
/// </summary>
/// <remarks>On top of CoreRuntime, handles all of the web-related runtime aspects of Umbraco.</remarks>
public class WebRuntime : CoreRuntime
{
private readonly IRequestCache _requestCache;
/// <summary>
/// Initializes a new instance of the <see cref="WebRuntime"/> class.
/// </summary>
public WebRuntime(
Configs configs,
IUmbracoVersion umbracoVersion,
IIOHelper ioHelper,
ILogger logger,
IProfiler profiler,
IHostingEnvironment hostingEnvironment,
IBackOfficeInfo backOfficeInfo,
IDbProviderFactoryCreator dbProviderFactoryCreator,
IMainDom mainDom,
ITypeFinder typeFinder,
IRequestCache requestCache,
IUmbracoBootPermissionChecker umbracoBootPermissionChecker):
base(configs, umbracoVersion, ioHelper, logger, profiler ,umbracoBootPermissionChecker, hostingEnvironment, backOfficeInfo, dbProviderFactoryCreator, mainDom, typeFinder)
{
_requestCache = requestCache;
}
/// <inheritdoc/>
public override IFactory Configure(IRegister register)
{
var profilingLogger = new ProfilingLogger(Logger, Profiler);
var umbracoVersion = new UmbracoVersion();
using (var timer = profilingLogger.TraceDuration<CoreRuntime>(
$"Booting Umbraco {umbracoVersion.SemanticVersion.ToSemanticString()}.",
"Booted.",
"Boot failed."))
{
Logger.Info<CoreRuntime>("Booting site '{HostingSiteName}', app '{HostingApplicationId}', path '{HostingPhysicalPath}', server '{MachineName}'.",
HostingEnvironment.SiteName,
HostingEnvironment.ApplicationId,
HostingEnvironment.ApplicationPhysicalPath,
NetworkHelper.MachineName);
Logger.Debug<CoreRuntime>("Runtime: {Runtime}", GetType().FullName);
var factory = base.Configure(register);
// now (and only now) is the time to switch over to perWebRequest scopes.
// up until that point we may not have a request, and scoped services would
// fail to resolve - but we run Initialize within a factory scope - and then,
// here, we switch the factory to bind scopes to requests
factory.EnablePerWebRequestScope();
return factory;
}
}
#region Getters
protected override AppCaches GetAppCaches() => new AppCaches(
// we need to have the dep clone runtime cache provider to ensure
// all entities are cached properly (cloned in and cloned out)
new DeepCloneAppCache(new ObjectCacheAppCache()),
// we need request based cache when running in web-based context
_requestCache,
new IsolatedCaches(type =>
// we need to have the dep clone runtime cache provider to ensure
// all entities are cached properly (cloned in and cloned out)
new DeepCloneAppCache(new ObjectCacheAppCache())));
#endregion
}
}
@@ -12,6 +12,7 @@ using Umbraco.Core.Persistence.Querying;
using Umbraco.Core.Persistence.Repositories;
using Umbraco.Core.Scoping;
using Umbraco.Core.Services.Changes;
using Umbraco.Core.Strings;
namespace Umbraco.Core.Services.Implement
{
@@ -27,6 +28,7 @@ namespace Umbraco.Core.Services.Implement
private readonly IDocumentBlueprintRepository _documentBlueprintRepository;
private readonly ILanguageRepository _languageRepository;
private readonly Lazy<IPropertyValidationService> _propertyValidationService;
private readonly IShortStringHelper _shortStringHelper;
private IQuery<IContent> _queryNotTrashed;
#region Constructors
@@ -35,7 +37,7 @@ namespace Umbraco.Core.Services.Implement
IEventMessagesFactory eventMessagesFactory,
IDocumentRepository documentRepository, IEntityRepository entityRepository, IAuditRepository auditRepository,
IContentTypeRepository contentTypeRepository, IDocumentBlueprintRepository documentBlueprintRepository, ILanguageRepository languageRepository,
Lazy<IPropertyValidationService> propertyValidationService)
Lazy<IPropertyValidationService> propertyValidationService, IShortStringHelper shortStringHelper)
: base(provider, logger, eventMessagesFactory)
{
_documentRepository = documentRepository;
@@ -45,6 +47,7 @@ namespace Umbraco.Core.Services.Implement
_documentBlueprintRepository = documentBlueprintRepository;
_languageRepository = languageRepository;
_propertyValidationService = propertyValidationService;
_shortStringHelper = shortStringHelper;
}
#endregion
@@ -600,23 +603,27 @@ namespace Umbraco.Core.Services.Implement
totalChildren = 0;
return Enumerable.Empty<IContent>();
}
return GetPagedDescendantsLocked(contentPath[0].Path, pageIndex, pageSize, out totalChildren, filter, ordering);
return GetPagedLocked(GetPagedDescendantQuery(contentPath[0].Path), pageIndex, pageSize, out totalChildren, filter, ordering);
}
return GetPagedDescendantsLocked(null, pageIndex, pageSize, out totalChildren, filter, ordering);
return GetPagedLocked(null, pageIndex, pageSize, out totalChildren, filter, ordering);
}
}
private IEnumerable<IContent> GetPagedDescendantsLocked(string contentPath, long pageIndex, int pageSize, out long totalChildren,
private IQuery<IContent> GetPagedDescendantQuery(string contentPath)
{
var query = Query<IContent>();
if (!contentPath.IsNullOrWhiteSpace())
query.Where(x => x.Path.SqlStartsWith($"{contentPath},", TextColumnType.NVarchar));
return query;
}
private IEnumerable<IContent> GetPagedLocked(IQuery<IContent> query, long pageIndex, int pageSize, out long totalChildren,
IQuery<IContent> filter, Ordering ordering)
{
if (pageIndex < 0) throw new ArgumentOutOfRangeException(nameof(pageIndex));
if (pageSize <= 0) throw new ArgumentOutOfRangeException(nameof(pageSize));
if (ordering == null) throw new ArgumentNullException(nameof(ordering));
var query = Query<IContent>();
if (!contentPath.IsNullOrWhiteSpace())
query.Where(x => x.Path.SqlStartsWith($"{contentPath},", TextColumnType.NVarchar));
return _documentRepository.GetPage(query, pageIndex, pageSize, out totalChildren, filter, ordering);
}
@@ -1865,7 +1872,7 @@ namespace Umbraco.Core.Services.Implement
public OperationResult MoveToRecycleBin(IContent content, int userId)
{
var evtMsgs = EventMessagesFactory.Get();
var moves = new List<Tuple<IContent, string>>();
var moves = new List<(IContent, string)>();
using (var scope = ScopeProvider.CreateScope())
{
@@ -1924,7 +1931,7 @@ namespace Umbraco.Core.Services.Implement
return;
}
var moves = new List<Tuple<IContent, string>>();
var moves = new List<(IContent, string)>();
using (var scope = ScopeProvider.CreateScope())
{
@@ -1977,7 +1984,7 @@ namespace Umbraco.Core.Services.Implement
// MUST be called from within WriteLock
// trash indicates whether we are trashing, un-trashing, or not changing anything
private void PerformMoveLocked(IContent content, int parentId, IContent parent, int userId,
ICollection<Tuple<IContent, string>> moves,
ICollection<(IContent, string)> moves,
bool? trash)
{
content.WriterId = userId;
@@ -1989,7 +1996,7 @@ namespace Umbraco.Core.Services.Implement
var paths = new Dictionary<int, string>();
moves.Add(Tuple.Create(content, content.Path)); // capture original path
moves.Add((content, content.Path)); // capture original path
//need to store the original path to lookup descendants based on it below
var originalPath = content.Path;
@@ -2006,20 +2013,24 @@ namespace Umbraco.Core.Services.Implement
paths[content.Id] = (parent == null ? (parentId == Constants.System.RecycleBinContent ? "-1,-20" : Constants.System.RootString) : parent.Path) + "," + content.Id;
const int pageSize = 500;
var total = long.MaxValue;
while (total > 0)
var query = GetPagedDescendantQuery(originalPath);
long total;
do
{
var descendants = GetPagedDescendantsLocked(originalPath, 0, pageSize, out total, null, Ordering.By("Path", Direction.Ascending));
// We always page a page 0 because for each page, we are moving the result so the resulting total will be reduced
var descendants = GetPagedLocked(query, 0, pageSize, out total, null, Ordering.By("Path", Direction.Ascending));
foreach (var descendant in descendants)
{
moves.Add(Tuple.Create(descendant, descendant.Path)); // capture original path
moves.Add((descendant, descendant.Path)); // capture original path
// update path and level since we do not update parentId
descendant.Path = paths[descendant.Id] = paths[descendant.ParentId] + "," + descendant.Id;
descendant.Level += levelDelta;
PerformMoveContentLocked(descendant, userId, trash);
}
}
} while (total > pageSize);
}
@@ -2367,6 +2378,25 @@ namespace Umbraco.Core.Services.Implement
return OperationResult.Succeed(evtMsgs);
}
public ContentDataIntegrityReport CheckDataIntegrity(ContentDataIntegrityReportOptions options)
{
using (var scope = ScopeProvider.CreateScope(autoComplete: true))
{
scope.WriteLock(Constants.Locks.ContentTree);
var report = _documentRepository.CheckDataIntegrity(options);
if (report.FixedIssues.Count > 0)
{
//The event args needs a content item so we'll make a fake one with enough properties to not cause a null ref
var root = new Content("root", -1, new ContentType(_shortStringHelper, -1)) {Id = -1, Key = Guid.Empty};
scope.Events.Dispatch(TreeChanged, this, new TreeChange<IContent>.EventArgs(new TreeChange<IContent>(root, TreeChangeTypes.RefreshAll)));
}
return report;
}
}
#endregion
#region Internal Methods
@@ -2804,7 +2834,7 @@ namespace Umbraco.Core.Services.Implement
// which we need for many things like keeping caches in sync, but we can surely do this MUCH better.
var changes = new List<TreeChange<IContent>>();
var moves = new List<Tuple<IContent, string>>();
var moves = new List<(IContent, string)>();
var contentTypeIdsA = contentTypeIds.ToArray();
// using an immediate uow here because we keep making changes with
@@ -13,6 +13,7 @@ using Umbraco.Core.Persistence.Querying;
using Umbraco.Core.Persistence.Repositories;
using Umbraco.Core.Scoping;
using Umbraco.Core.Services.Changes;
using Umbraco.Core.Strings;
namespace Umbraco.Core.Services.Implement
{
@@ -25,6 +26,7 @@ namespace Umbraco.Core.Services.Implement
private readonly IMediaTypeRepository _mediaTypeRepository;
private readonly IAuditRepository _auditRepository;
private readonly IEntityRepository _entityRepository;
private readonly IShortStringHelper _shortStringHelper;
private readonly IMediaFileSystem _mediaFileSystem;
@@ -32,7 +34,7 @@ namespace Umbraco.Core.Services.Implement
public MediaService(IScopeProvider provider, IMediaFileSystem mediaFileSystem, ILogger logger, IEventMessagesFactory eventMessagesFactory,
IMediaRepository mediaRepository, IAuditRepository auditRepository, IMediaTypeRepository mediaTypeRepository,
IEntityRepository entityRepository)
IEntityRepository entityRepository, IShortStringHelper shortStringHelper)
: base(provider, logger, eventMessagesFactory)
{
_mediaFileSystem = mediaFileSystem;
@@ -40,6 +42,7 @@ namespace Umbraco.Core.Services.Implement
_auditRepository = auditRepository;
_mediaTypeRepository = mediaTypeRepository;
_entityRepository = entityRepository;
_shortStringHelper = shortStringHelper;
}
#endregion
@@ -530,23 +533,27 @@ namespace Umbraco.Core.Services.Implement
totalChildren = 0;
return Enumerable.Empty<IMedia>();
}
return GetPagedDescendantsLocked(mediaPath[0].Path, pageIndex, pageSize, out totalChildren, filter, ordering);
return GetPagedLocked(GetPagedDescendantQuery(mediaPath[0].Path), pageIndex, pageSize, out totalChildren, filter, ordering);
}
return GetPagedDescendantsLocked(null, pageIndex, pageSize, out totalChildren, filter, ordering);
return GetPagedLocked(GetPagedDescendantQuery(null), pageIndex, pageSize, out totalChildren, filter, ordering);
}
}
private IEnumerable<IMedia> GetPagedDescendantsLocked(string mediaPath, long pageIndex, int pageSize, out long totalChildren,
private IQuery<IMedia> GetPagedDescendantQuery(string mediaPath)
{
var query = Query<IMedia>();
if (!mediaPath.IsNullOrWhiteSpace())
query.Where(x => x.Path.SqlStartsWith(mediaPath + ",", TextColumnType.NVarchar));
return query;
}
private IEnumerable<IMedia> GetPagedLocked(IQuery<IMedia> query, long pageIndex, int pageSize, out long totalChildren,
IQuery<IMedia> filter, Ordering ordering)
{
if (pageIndex < 0) throw new ArgumentOutOfRangeException(nameof(pageIndex));
if (pageSize <= 0) throw new ArgumentOutOfRangeException(nameof(pageSize));
if (ordering == null) throw new ArgumentNullException(nameof(ordering));
var query = Query<IMedia>();
if (!mediaPath.IsNullOrWhiteSpace())
query.Where(x => x.Path.SqlStartsWith(mediaPath + ",", TextColumnType.NVarchar));
return _mediaRepository.GetPage(query, pageIndex, pageSize, out totalChildren, filter, ordering);
}
@@ -888,7 +895,7 @@ namespace Umbraco.Core.Services.Implement
public Attempt<OperationResult> MoveToRecycleBin(IMedia media, int userId = Constants.Security.SuperUserId)
{
var evtMsgs = EventMessagesFactory.Get();
var moves = new List<Tuple<IMedia, string>>();
var moves = new List<(IMedia, string)>();
using (var scope = ScopeProvider.CreateScope())
{
@@ -940,7 +947,7 @@ namespace Umbraco.Core.Services.Implement
return OperationResult.Attempt.Succeed(evtMsgs);
}
var moves = new List<Tuple<IMedia, string>>();
var moves = new List<(IMedia, string)>();
using (var scope = ScopeProvider.CreateScope())
{
@@ -979,7 +986,7 @@ namespace Umbraco.Core.Services.Implement
// MUST be called from within WriteLock
// trash indicates whether we are trashing, un-trashing, or not changing anything
private void PerformMoveLocked(IMedia media, int parentId, IMedia parent, int userId, ICollection<Tuple<IMedia, string>> moves, bool? trash)
private void PerformMoveLocked(IMedia media, int parentId, IMedia parent, int userId, ICollection<(IMedia, string)> moves, bool? trash)
{
media.ParentId = parentId;
@@ -989,7 +996,7 @@ namespace Umbraco.Core.Services.Implement
var paths = new Dictionary<int, string>();
moves.Add(Tuple.Create(media, media.Path)); // capture original path
moves.Add((media, media.Path)); // capture original path
//need to store the original path to lookup descendants based on it below
var originalPath = media.Path;
@@ -1006,21 +1013,25 @@ namespace Umbraco.Core.Services.Implement
paths[media.Id] = (parent == null ? (parentId == Constants.System.RecycleBinMedia ? "-1,-21" : Constants.System.RootString) : parent.Path) + "," + media.Id;
const int pageSize = 500;
var page = 0;
var total = long.MaxValue;
while (page * pageSize < total)
var query = GetPagedDescendantQuery(originalPath);
long total;
do
{
var descendants = GetPagedDescendantsLocked(originalPath, page++, pageSize, out total, null, Ordering.By("Path", Direction.Ascending));
// We always page a page 0 because for each page, we are moving the result so the resulting total will be reduced
var descendants = GetPagedLocked(query, 0, pageSize, out total, null, Ordering.By("Path", Direction.Ascending));
foreach (var descendant in descendants)
{
moves.Add(Tuple.Create(descendant, descendant.Path)); // capture original path
moves.Add((descendant, descendant.Path)); // capture original path
// update path and level since we do not update parentId
descendant.Path = paths[descendant.Id] = paths[descendant.ParentId] + "," + descendant.Id;
descendant.Level += levelDelta;
PerformMoveMediaLocked(descendant, userId, trash);
}
}
} while (total > pageSize);
}
private void PerformMoveMediaLocked(IMedia media, int userId, bool? trash)
@@ -1132,6 +1143,26 @@ namespace Umbraco.Core.Services.Implement
}
return true;
}
public ContentDataIntegrityReport CheckDataIntegrity(ContentDataIntegrityReportOptions options)
{
using (var scope = ScopeProvider.CreateScope(autoComplete: true))
{
scope.WriteLock(Constants.Locks.MediaTree);
var report = _mediaRepository.CheckDataIntegrity(options);
if (report.FixedIssues.Count > 0)
{
//The event args needs a content item so we'll make a fake one with enough properties to not cause a null ref
var root = new Models.Media("root", -1, new MediaType(_shortStringHelper, -1)) { Id = -1, Key = Guid.Empty };
scope.Events.Dispatch(TreeChanged, this, new TreeChange<IMedia>.EventArgs(new TreeChange<IMedia>(root, TreeChangeTypes.RefreshAll)));
}
return report;
}
}
#endregion
@@ -1270,7 +1301,7 @@ namespace Umbraco.Core.Services.Implement
// which we need for many things like keeping caches in sync, but we can surely do this MUCH better.
var changes = new List<TreeChange<IMedia>>();
var moves = new List<Tuple<IMedia, string>>();
var moves = new List<(IMedia, string)>();
var mediaTypeIdsA = mediaTypeIds.ToArray();
using (var scope = ScopeProvider.CreateScope())
@@ -1351,5 +1382,7 @@ namespace Umbraco.Core.Services.Implement
}
#endregion
}
}
@@ -505,6 +505,14 @@ namespace Umbraco.Web.PublishedCache.NuCache
}
}
/// <summary>
/// Validate the <see cref="ContentNodeKit"/> and try to create a parent <see cref="LinkedNode{ContentNode}"/>
/// </summary>
/// <param name="kit"></param>
/// <param name="parent"></param>
/// <returns>
/// Returns false if the parent was not found or if the kit validation failed
/// </returns>
private bool BuildKit(ContentNodeKit kit, out LinkedNode<ContentNode> parent)
{
// make sure parent exists
@@ -515,6 +523,15 @@ namespace Umbraco.Web.PublishedCache.NuCache
return false;
}
// We cannot continue if there's no value. This shouldn't happen but it can happen if the database umbracoNode.path
// data is invalid/corrupt. If that is the case, the parentId might be ok but not the Path which can result in null
// because the data sort operation is by path.
if (parent.Value == null)
{
_logger.Warn<ContentStore>($"Skip item id={kit.Node.Id}, no Data assigned for linked node with path {kit.Node.Path} and parent id {kit.Node.ParentContentId}. This can indicate data corruption for the Path value for node {kit.Node.Id}. See the Health Check dashboard in Settings to resolve data integrity issues.");
return false;
}
// make sure the kit is valid
if (kit.DraftData == null && kit.PublishedData == null)
{
@@ -803,7 +820,7 @@ namespace Umbraco.Web.PublishedCache.NuCache
{
//this zero's out the branch (recursively), if we're in a new gen this will add a NULL placeholder for the gen
ClearBranchLocked(existing);
//TODO: This removes the current GEN from the tree - do we really want to do that?
//TODO: This removes the current GEN from the tree - do we really want to do that? (not sure if this is still an issue....)
RemoveTreeNodeLocked(existing);
}
@@ -868,6 +885,10 @@ namespace Umbraco.Web.PublishedCache.NuCache
private void ClearBranchLocked(ContentNode content)
{
// This should never be null, all code that calls this method is null checking but we've seen
// issues of null ref exceptions in issue reports so we'll double check here
if (content == null) throw new ArgumentNullException(nameof(content));
SetValueLocked(_contentNodes, content.Id, null);
if (_localDb != null) RegisterChange(content.Id, ContentNodeKit.Null);
@@ -1035,6 +1056,12 @@ namespace Umbraco.Web.PublishedCache.NuCache
var parent = parentLink.Value;
// We are doing a null check here but this should no longer be possible because we have a null check in BuildKit
// for the parent.Value property and we'll output a warning. However I'll leave this additional null check in place.
// see https://github.com/umbraco/Umbraco-CMS/issues/7868
if (parent == null)
throw new PanicException($"A null Value was returned on the {nameof(parentLink)} LinkedNode with id={content.ParentContentId}, potentially your database paths are corrupted.");
// if parent has no children, clone parent + add as first child
if (parent.FirstChildContentId < 0)
{
@@ -11,7 +11,7 @@
{
public LinkedNode(TValue value, long gen, LinkedNode<TValue> next = null)
{
Value = value;
Value = value; // This is allowed to be null, we actually explicitly set this to null in ClearLocked
Gen = gen;
Next = next;
}
@@ -152,5 +152,14 @@ namespace Umbraco.Tests.Common
return mock.Object;
}
public ILoggingConfiguration GetLoggingConfiguration(IHostingEnvironment hostingEnv = null)
{
hostingEnv = hostingEnv ?? GetHostingEnvironment();
return new LoggingConfiguration(
Path.Combine(hostingEnv.ApplicationPhysicalPath, "App_Data\\Logs"),
Path.Combine(hostingEnv.ApplicationPhysicalPath, "config\\serilog.config"),
Path.Combine(hostingEnv.ApplicationPhysicalPath, "config\\serilog.user.config"));
}
}
}
@@ -1,6 +1,7 @@
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Umbraco.Core.Configuration;
using Umbraco.Core.Logging;
using Umbraco.Web.Common.AspNetCore;
namespace Umbraco.Tests.Integration.Implementations
@@ -4,8 +4,10 @@ using Moq;
using NUnit.Framework;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Smidge;
using Umbraco.Core;
using Umbraco.Core.Cache;
using Umbraco.Core.Composing;
using Umbraco.Core.Logging;
using Umbraco.Core.Runtime;
@@ -57,7 +59,7 @@ namespace Umbraco.Tests.Integration
var coreRuntime = new CoreRuntime(testHelper.GetConfigs(), testHelper.GetUmbracoVersion(),
testHelper.IOHelper, testHelper.Logger, testHelper.Profiler, testHelper.UmbracoBootPermissionChecker,
testHelper.GetHostingEnvironment(), testHelper.GetBackOfficeInfo(), testHelper.DbProviderFactoryCreator,
testHelper.MainDom, testHelper.GetTypeFinder());
testHelper.MainDom, testHelper.GetTypeFinder(), NoAppCache.Instance);
// boot it!
var factory = coreRuntime.Configure(umbracoContainer);
@@ -99,7 +101,7 @@ namespace Umbraco.Tests.Integration
// Add it!
services.AddUmbracoConfiguration(hostContext.Configuration);
services.AddUmbracoCore(webHostEnvironment, umbracoContainer, GetType().Assembly, out _);
services.AddUmbracoCore(webHostEnvironment, umbracoContainer, GetType().Assembly, NoAppCache.Instance, testHelper.GetLoggingConfiguration(), out _);
});
var host = await hostBuilder.StartAsync();
@@ -138,7 +140,7 @@ namespace Umbraco.Tests.Integration
// Add it!
services.AddUmbracoConfiguration(hostContext.Configuration);
services.AddUmbracoCore(webHostEnvironment, umbracoContainer, GetType().Assembly, out _);
services.AddUmbracoCore(webHostEnvironment, umbracoContainer, GetType().Assembly, NoAppCache.Instance, testHelper.GetLoggingConfiguration(), out _);
});
var host = await hostBuilder.StartAsync();
@@ -108,7 +108,7 @@ namespace Umbraco.Tests.Integration.Testing
// Add it!
services.AddUmbracoConfiguration(hostContext.Configuration);
services.AddUmbracoCore(webHostEnvironment, umbracoContainer, GetType().Assembly, out _);
services.AddUmbracoCore(webHostEnvironment, umbracoContainer, GetType().Assembly, NoAppCache.Instance, testHelper.GetLoggingConfiguration(), out _);
});
var host = await hostBuilder.StartAsync();
+9 -3
View File
@@ -1,9 +1,11 @@
using Moq;
using NUnit.Framework;
using Serilog;
using System;
using System.IO;
using System.Linq;
using Umbraco.Core;
using Umbraco.Core.Logging;
using Umbraco.Core.Logging.Viewer;
using Umbraco.Tests.TestHelpers;
@@ -33,13 +35,16 @@ namespace Umbraco.Tests.Logging
//Create an example JSON log file to check results
//As a one time setup for all tets in this class/fixture
var ioHelper = TestHelper.IOHelper;
var hostingEnv = TestHelper.GetHostingEnvironment();
var loggingConfiguration = TestHelper.GetLoggingConfiguration(hostingEnv);
var exampleLogfilePath = Path.Combine(TestContext.CurrentContext.TestDirectory, @"Logging\", _logfileName);
_newLogfileDirPath = Path.Combine(TestContext.CurrentContext.TestDirectory, @"App_Data\Logs\");
_newLogfileDirPath = loggingConfiguration.LogDirectory;
_newLogfilePath = Path.Combine(_newLogfileDirPath, _logfileName);
var exampleSearchfilePath = Path.Combine(TestContext.CurrentContext.TestDirectory, @"Logging\", _searchfileName);
_newSearchfileDirPath = Path.Combine(TestContext.CurrentContext.TestDirectory, @"Config\");
_newSearchfileDirPath = Path.Combine(hostingEnv.ApplicationPhysicalPath, @"Config\");
_newSearchfilePath = Path.Combine(_newSearchfileDirPath, _searchfileName);
//Create/ensure Directory exists
@@ -51,7 +56,8 @@ namespace Umbraco.Tests.Logging
File.Copy(exampleSearchfilePath, _newSearchfilePath, true);
var logger = Mock.Of<Core.Logging.ILogger>();
_logViewer = new JsonLogViewer(logger, ioHelper, logsPath: _newLogfileDirPath, searchPath: _newSearchfilePath);
var logViewerConfig = new LogViewerConfig(hostingEnv);
_logViewer = new SerilogJsonLogViewer(logger, logViewerConfig, loggingConfiguration, Log.Logger);
}
[OneTimeTearDown]
@@ -16,10 +16,10 @@ using Umbraco.Web.Mvc;
using Umbraco.Web.WebApi;
using Umbraco.Core.Strings;
using Umbraco.Core.Configuration;
using Umbraco.Core.Dictionary;
using Umbraco.Core.Hosting;
using Umbraco.Core.IO;
using Umbraco.Core.Models.PublishedContent;
using Umbraco.Core.Runtime;
using Umbraco.Core.Services;
using Umbraco.Tests.PublishedContent;
using Umbraco.Tests.Testing;
@@ -47,10 +47,10 @@ namespace Umbraco.Tests.Routing
HostingEnvironment);
}
public class TestRuntime : WebRuntime
public class TestRuntime : CoreRuntime
{
public TestRuntime(Configs configs, IUmbracoVersion umbracoVersion, IIOHelper ioHelper, ILogger logger, IHostingEnvironment hostingEnvironment, IBackOfficeInfo backOfficeInfo)
: base(configs, umbracoVersion, ioHelper, Mock.Of<ILogger>(), Mock.Of<IProfiler>(), hostingEnvironment, backOfficeInfo, TestHelper.DbProviderFactoryCreator, TestHelper.MainDom, TestHelper.GetTypeFinder(), TestHelper.GetRequestCache(), new AspNetUmbracoBootPermissionChecker())
: base(configs, umbracoVersion, ioHelper, Mock.Of<ILogger>(), Mock.Of<IProfiler>(), new AspNetUmbracoBootPermissionChecker(), hostingEnvironment, backOfficeInfo, TestHelper.DbProviderFactoryCreator, TestHelper.MainDom, TestHelper.GetTypeFinder(), NoAppCache.Instance)
{
}
@@ -5,6 +5,7 @@ using Examine;
using Moq;
using NUnit.Framework;
using Umbraco.Core;
using Umbraco.Core.Cache;
using Umbraco.Core.Composing;
using Umbraco.Core.Configuration;
using Umbraco.Core.Configuration.UmbracoSettings;
@@ -16,6 +17,7 @@ using Umbraco.Core.Logging;
using Umbraco.Core.Persistence;
using Umbraco.Core.Runtime;
using Umbraco.Core.Scoping;
using Umbraco.Net;
using Umbraco.Tests.TestHelpers;
using Umbraco.Tests.TestHelpers.Stubs;
using Umbraco.Web;
@@ -116,7 +118,7 @@ namespace Umbraco.Tests.Runtimes
public class TestRuntime : CoreRuntime
{
public TestRuntime(Configs configs, IUmbracoVersion umbracoVersion, IIOHelper ioHelper, ILogger logger, IProfiler profiler, IHostingEnvironment hostingEnvironment, IBackOfficeInfo backOfficeInfo)
:base(configs, umbracoVersion, ioHelper, logger, profiler, new AspNetUmbracoBootPermissionChecker(), hostingEnvironment, backOfficeInfo, TestHelper.DbProviderFactoryCreator, TestHelper.MainDom, TestHelper.GetTypeFinder())
:base(configs, umbracoVersion, ioHelper, logger, profiler, new AspNetUmbracoBootPermissionChecker(), hostingEnvironment, backOfficeInfo, TestHelper.DbProviderFactoryCreator, TestHelper.MainDom, TestHelper.GetTypeFinder(), NoAppCache.Instance)
{
}
@@ -134,6 +136,7 @@ namespace Umbraco.Tests.Runtimes
public override IFactory Configure(IRegister container)
{
container.Register<IApplicationShutdownRegistry, AspNetApplicationShutdownRegistry>(Lifetime.Singleton);
container.Register<ISessionIdResolver, NullSessionIdResolver>(Lifetime.Singleton);
var factory = base.Configure(container);
return factory;
@@ -76,7 +76,7 @@ namespace Umbraco.Tests.Runtimes
var runtimeState = new RuntimeState(logger, null, umbracoVersion, backOfficeInfo);
var configs = TestHelper.GetConfigs();
var variationContextAccessor = TestHelper.VariationContextAccessor;
// create the register and the composition
var register = TestHelper.GetRegister();
@@ -84,7 +84,7 @@ namespace Umbraco.Tests.Runtimes
composition.RegisterEssentials(logger, profiler, profilingLogger, mainDom, appCaches, databaseFactory, typeLoader, runtimeState, typeFinder, ioHelper, umbracoVersion, TestHelper.DbProviderFactoryCreator, hostingEnvironment, backOfficeInfo);
// create the core runtime and have it compose itself
var coreRuntime = new CoreRuntime(configs, umbracoVersion, ioHelper, logger, profiler, new AspNetUmbracoBootPermissionChecker(), hostingEnvironment, backOfficeInfo, TestHelper.DbProviderFactoryCreator, TestHelper.MainDom, typeFinder);
var coreRuntime = new CoreRuntime(configs, umbracoVersion, ioHelper, logger, profiler, new AspNetUmbracoBootPermissionChecker(), hostingEnvironment, backOfficeInfo, TestHelper.DbProviderFactoryCreator, TestHelper.MainDom, typeFinder, NoAppCache.Instance);
// determine actual runtime level
runtimeState.DetermineRuntimeLevel(databaseFactory, logger);
@@ -278,7 +278,7 @@ namespace Umbraco.Tests.Runtimes
composition.RegisterEssentials(logger, profiler, profilingLogger, mainDom, appCaches, databaseFactory, typeLoader, runtimeState, typeFinder, ioHelper, umbracoVersion, TestHelper.DbProviderFactoryCreator, hostingEnvironment, backOfficeInfo);
// create the core runtime and have it compose itself
var coreRuntime = new CoreRuntime(configs, umbracoVersion, ioHelper, logger, profiler, new AspNetUmbracoBootPermissionChecker(), hostingEnvironment, backOfficeInfo, TestHelper.DbProviderFactoryCreator, TestHelper.MainDom, typeFinder);
var coreRuntime = new CoreRuntime(configs, umbracoVersion, ioHelper, logger, profiler, new AspNetUmbracoBootPermissionChecker(), hostingEnvironment, backOfficeInfo, TestHelper.DbProviderFactoryCreator, TestHelper.MainDom, typeFinder, NoAppCache.Instance);
// get the components
// all of them?
@@ -322,8 +322,8 @@ namespace Umbraco.Tests.Runtimes
Assert.AreEqual(0, results.Count);
}
}
}
@@ -19,11 +19,6 @@ namespace Umbraco.Tests.TestHelpers.Stubs
private static bool _enabled;
public string Render()
{
return string.Empty;
}
public IDisposable Step(string name)
{
return _enabled ? MiniProfiler.Current.Step(name) : null;
@@ -324,6 +324,8 @@ namespace Umbraco.Tests.TestHelpers
public static IHostingEnvironment GetHostingEnvironment() => _testHelperInternal.GetHostingEnvironment();
public static ILoggingConfiguration GetLoggingConfiguration(IHostingEnvironment hostingEnv) => _testHelperInternal.GetLoggingConfiguration(hostingEnv);
public static IApplicationShutdownRegistry GetHostingEnvironmentLifetime() => _testHelperInternal.GetHostingEnvironmentLifetime();
public static IIpResolver GetIpResolver() => _testHelperInternal.GetIpResolver();
+2 -2
View File
@@ -158,12 +158,12 @@ namespace Umbraco.Tests.TestHelpers
var userService = GetLazyService<IUserService>(factory, c => new UserService(scopeProvider, logger, eventMessagesFactory, runtimeState, GetRepo<IUserRepository>(c), GetRepo<IUserGroupRepository>(c),globalSettings));
var dataTypeService = GetLazyService<IDataTypeService>(factory, c => new DataTypeService(scopeProvider, logger, eventMessagesFactory, GetRepo<IDataTypeRepository>(c), GetRepo<IDataTypeContainerRepository>(c), GetRepo<IAuditRepository>(c), GetRepo<IEntityRepository>(c), GetRepo<IContentTypeRepository>(c), ioHelper, localizedTextService.Value, localizationService.Value, TestHelper.ShortStringHelper));
var propertyValidationService = new Lazy<IPropertyValidationService>(() => new PropertyValidationService(propertyEditorCollection, dataTypeService.Value));
var contentService = GetLazyService<IContentService>(factory, c => new ContentService(scopeProvider, logger, eventMessagesFactory, GetRepo<IDocumentRepository>(c), GetRepo<IEntityRepository>(c), GetRepo<IAuditRepository>(c), GetRepo<IContentTypeRepository>(c), GetRepo<IDocumentBlueprintRepository>(c), GetRepo<ILanguageRepository>(c), propertyValidationService));
var contentService = GetLazyService<IContentService>(factory, c => new ContentService(scopeProvider, logger, eventMessagesFactory, GetRepo<IDocumentRepository>(c), GetRepo<IEntityRepository>(c), GetRepo<IAuditRepository>(c), GetRepo<IContentTypeRepository>(c), GetRepo<IDocumentBlueprintRepository>(c), GetRepo<ILanguageRepository>(c), propertyValidationService, TestHelper.ShortStringHelper));
var notificationService = GetLazyService<INotificationService>(factory, c => new NotificationService(scopeProvider, userService.Value, contentService.Value, localizationService.Value, logger, ioHelper, GetRepo<INotificationsRepository>(c), globalSettings, contentSettings));
var serverRegistrationService = GetLazyService<IServerRegistrationService>(factory, c => new ServerRegistrationService(scopeProvider, logger, eventMessagesFactory, GetRepo<IServerRegistrationRepository>(c), TestHelper.GetHostingEnvironment()));
var memberGroupService = GetLazyService<IMemberGroupService>(factory, c => new MemberGroupService(scopeProvider, logger, eventMessagesFactory, GetRepo<IMemberGroupRepository>(c)));
var memberService = GetLazyService<IMemberService>(factory, c => new MemberService(scopeProvider, logger, eventMessagesFactory, memberGroupService.Value, GetRepo<IMemberRepository>(c), GetRepo<IMemberTypeRepository>(c), GetRepo<IMemberGroupRepository>(c), GetRepo<IAuditRepository>(c)));
var mediaService = GetLazyService<IMediaService>(factory, c => new MediaService(scopeProvider, mediaFileSystem, logger, eventMessagesFactory, GetRepo<IMediaRepository>(c), GetRepo<IAuditRepository>(c), GetRepo<IMediaTypeRepository>(c), GetRepo<IEntityRepository>(c)));
var mediaService = GetLazyService<IMediaService>(factory, c => new MediaService(scopeProvider, mediaFileSystem, logger, eventMessagesFactory, GetRepo<IMediaRepository>(c), GetRepo<IAuditRepository>(c), GetRepo<IMediaTypeRepository>(c), GetRepo<IEntityRepository>(c), TestHelper.ShortStringHelper));
var contentTypeService = GetLazyService<IContentTypeService>(factory, c => new ContentTypeService(scopeProvider, logger, eventMessagesFactory, contentService.Value, GetRepo<IContentTypeRepository>(c), GetRepo<IAuditRepository>(c), GetRepo<IDocumentTypeContainerRepository>(c), GetRepo<IEntityRepository>(c)));
var mediaTypeService = GetLazyService<IMediaTypeService>(factory, c => new MediaTypeService(scopeProvider, logger, eventMessagesFactory, mediaService.Value, GetRepo<IMediaTypeRepository>(c), GetRepo<IAuditRepository>(c), GetRepo<IMediaTypeContainerRepository>(c), GetRepo<IEntityRepository>(c)));
var fileService = GetLazyService<IFileService>(factory, c => new FileService(scopeProvider, ioHelper, logger, eventMessagesFactory, GetRepo<IStylesheetRepository>(c), GetRepo<IScriptRepository>(c), GetRepo<ITemplateRepository>(c), GetRepo<IPartialViewRepository>(c), GetRepo<IPartialViewMacroRepository>(c), GetRepo<IAuditRepository>(c), TestHelper.ShortStringHelper, globalSettings));
+1 -1
View File
@@ -264,7 +264,7 @@ namespace Umbraco.Tests.Testing
profiler = Mock.Of<IProfiler>();
break;
case UmbracoTestOptions.Logger.Serilog:
logger = new SerilogLogger(TestHelper.CoreDebugSettings, IOHelper, TestHelper.Marchal, new FileInfo(TestHelper.MapPathForTestFiles("~/unit-test.config")));
logger = new SerilogLogger(new FileInfo(TestHelper.MapPathForTestFiles("~/unit-test.config")));
profiler = new LogProfiler(logger);
break;
case UmbracoTestOptions.Logger.Console:
@@ -18,7 +18,7 @@ namespace Umbraco.Tests.UmbracoExamine
public void InitializeFixture()
{
var logger = new SerilogLogger(TestHelper.CoreDebugSettings, IOHelper, TestHelper.Marchal, new FileInfo(TestHelper.MapPathForTestFiles("~/unit-test.config")));
var logger = new SerilogLogger(new FileInfo(TestHelper.MapPathForTestFiles("~/unit-test.config")));
_profilingLogger = new ProfilingLogger(logger, new LogProfiler(logger));
}
@@ -1,9 +1,13 @@
using System;
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;
using Serilog.Context;
using Smidge;
using Umbraco.Core;
using Umbraco.Core.Configuration;
using Umbraco.Core.Hosting;
using Umbraco.Infrastructure.Logging.Serilog.Enrichers;
using Umbraco.Web.Common.Middleware;
namespace Umbraco.Web.BackOffice.AspNetCore
{
@@ -31,6 +35,10 @@ namespace Umbraco.Web.BackOffice.AspNetCore
var runtimeShutdown = new CoreRuntimeShutdown(runtime, hostLifetime);
hostLifetime.RegisterObject(runtimeShutdown);
// Register our global threadabort enricher for logging
var threadAbortEnricher = app.ApplicationServices.GetRequiredService<ThreadAbortExceptionEnricher>();
LogContext.Push(threadAbortEnricher); // NOTE: We are not in a using clause because we are not removing it, it is on the global context
// Start the runtime!
runtime.Start();
@@ -64,6 +72,15 @@ namespace Umbraco.Web.BackOffice.AspNetCore
}
}
public static IApplicationBuilder UseUmbracoRequestLogging(this IApplicationBuilder app)
{
if (app == null) throw new ArgumentNullException(nameof(app));
app.UseMiddleware<UmbracoRequestLoggingMiddleware>();
return app;
}
public static IApplicationBuilder UseUmbracoRuntimeMinification(this IApplicationBuilder app)
{
if (app == null) throw new ArgumentNullException(nameof(app));
@@ -2,9 +2,7 @@
using Microsoft.AspNetCore.Mvc.Filters;
using Umbraco.Core.Hosting;
using Microsoft.Extensions.DependencyInjection;
using Umbraco.Core.Runtime;
using Umbraco.Core.WebAssets;
using Umbraco.Web.BackOffice.Controllers;
using Umbraco.Web.Common.ActionResults;
namespace Umbraco.Web.BackOffice.Filters
@@ -14,10 +12,11 @@ namespace Umbraco.Web.BackOffice.Filters
public override async Task OnResultExecutionAsync(ResultExecutingContext context, ResultExecutionDelegate next)
{
// logic before action goes here
var hostingEnvironment = context.HttpContext.RequestServices.GetService<IHostingEnvironment>();
var serviceProvider = context.HttpContext.RequestServices;
var hostingEnvironment = serviceProvider.GetService<IHostingEnvironment>();
if (!hostingEnvironment.IsDebugMode)
{
var runtimeMinifier = context.HttpContext.RequestServices.GetService<IRuntimeMinifier>();
var runtimeMinifier = serviceProvider.GetService<IRuntimeMinifier>();
if (context.Result is JavaScriptResult jsResult)
{
@@ -5,13 +5,12 @@ using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Umbraco.Core;
using Umbraco.Core.Configuration;
using Umbraco.Core.Logging;
namespace Umbraco.Web.Common.AspNetCore
{
public class AspNetCoreHostingEnvironment : Umbraco.Core.Hosting.IHostingEnvironment
{
private readonly IHostingSettings _hostingSettings;
private readonly IWebHostEnvironment _webHostEnvironment;
@@ -28,6 +27,7 @@ namespace Umbraco.Web.Common.AspNetCore
ApplicationVirtualPath = "/"; //TODO how to find this, This is a server thing, not application thing.
IISVersion = new Version(0, 0); // TODO not necessary IIS
}
public bool IsHosted { get; } = true;
@@ -0,0 +1,12 @@
namespace Umbraco.Web.Common.Constants
{
/// <summary>
/// constants
/// </summary>
internal static class ViewConstants
{
internal const string ViewLocation = "~/Views";
internal const string DataTokenCurrentViewContext = "umbraco-current-view-context";
}
}
@@ -0,0 +1,9 @@
using Microsoft.AspNetCore.Mvc;
namespace Umbraco.Web.Common.Controllers
{
public abstract class RenderController : Controller
{
}
}
@@ -0,0 +1,17 @@
using System;
using Microsoft.AspNetCore.Mvc;
namespace Umbraco.Web.Common.Events
{
public class ActionExecutedEventArgs : EventArgs
{
public Controller Controller { get; set; }
public object Model { get; set; }
public ActionExecutedEventArgs(Controller controller, object model)
{
Controller = controller;
Model = model;
}
}
}
@@ -1,4 +1,6 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Data.Common;
using System.IO;
using System.Reflection;
@@ -6,6 +8,10 @@ using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Serilog;
using Serilog.Extensions.Hosting;
using Serilog.Extensions.Logging;
using Smidge;
using Smidge.Nuglify;
using Umbraco.Composing;
@@ -71,7 +77,22 @@ namespace Umbraco.Web.Common.Extensions
var umbContainer = UmbracoServiceProviderFactory.UmbracoContainer;
services.AddUmbracoCore(webHostEnvironment, umbContainer, Assembly.GetEntryAssembly(), out factory);
var loggingConfig = new LoggingConfiguration(
Path.Combine(webHostEnvironment.ContentRootPath, "App_Data\\Logs"),
Path.Combine(webHostEnvironment.ContentRootPath, "config\\serilog.config"),
Path.Combine(webHostEnvironment.ContentRootPath, "config\\serilog.user.config"));
IHttpContextAccessor httpContextAccessor = new HttpContextAccessor();
services.AddSingleton<IHttpContextAccessor>(httpContextAccessor);
var requestCache = new GenericDictionaryRequestAppCache(() => httpContextAccessor.HttpContext.Items);
services.AddUmbracoCore(webHostEnvironment,
umbContainer,
Assembly.GetEntryAssembly(),
requestCache,
loggingConfig,
out factory);
return services;
}
@@ -83,20 +104,34 @@ namespace Umbraco.Web.Common.Extensions
/// <param name="webHostEnvironment"></param>
/// <param name="umbContainer"></param>
/// <param name="entryAssembly"></param>
/// <param name="requestCache"></param>
/// <param name="httpContextAccessor"></param>
/// <param name="loggingConfiguration"></param>
/// <param name="factory"></param>
/// <returns></returns>
public static IServiceCollection AddUmbracoCore(this IServiceCollection services, IWebHostEnvironment webHostEnvironment, IRegister umbContainer, Assembly entryAssembly, out IFactory factory)
public static IServiceCollection AddUmbracoCore(
this IServiceCollection services,
IWebHostEnvironment webHostEnvironment,
IRegister umbContainer,
Assembly entryAssembly,
IRequestCache requestCache,
ILoggingConfiguration loggingConfiguration,
out IFactory factory)
{
if (services is null) throw new ArgumentNullException(nameof(services));
var container = umbContainer;
if (container is null) throw new ArgumentNullException(nameof(container));
if (entryAssembly is null) throw new ArgumentNullException(nameof(entryAssembly));
// Special case! The generic host adds a few default services but we need to manually add this one here NOW because
// we resolve it before the host finishes configuring in the call to CreateCompositionRoot
services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
var serviceProvider = services.BuildServiceProvider();
var configs = serviceProvider.GetService<Configs>();
CreateCompositionRoot(services, webHostEnvironment, out var logger, out var configs, out var ioHelper, out var hostingEnvironment, out var backOfficeInfo, out var profiler);
CreateCompositionRoot(services,
configs,
webHostEnvironment,
loggingConfiguration,
out var logger, out var ioHelper, out var hostingEnvironment, out var backOfficeInfo, out var profiler);
var globalSettings = configs.Global();
var umbracoVersion = new UmbracoVersion(globalSettings);
@@ -109,14 +144,15 @@ namespace Umbraco.Web.Common.Extensions
profiler,
hostingEnvironment,
backOfficeInfo,
CreateTypeFinder(logger, profiler, webHostEnvironment, entryAssembly));
CreateTypeFinder(logger, profiler, webHostEnvironment, entryAssembly),
requestCache);
factory = coreRuntime.Configure(container);
return services;
}
private static ITypeFinder CreateTypeFinder(ILogger logger, IProfiler profiler, IWebHostEnvironment webHostEnvironment, Assembly entryAssembly)
private static ITypeFinder CreateTypeFinder(Core.Logging.ILogger logger, IProfiler profiler, IWebHostEnvironment webHostEnvironment, Assembly entryAssembly)
{
// TODO: Currently we are not passing in any TypeFinderConfig (with ITypeFinderSettings) which we should do, however
// this is not critical right now and would require loading in some config before boot time so just leaving this as-is for now.
@@ -126,11 +162,12 @@ namespace Umbraco.Web.Common.Extensions
return new TypeFinder(logger, new DefaultUmbracoAssemblyProvider(entryAssembly), runtimeHash);
}
private static IRuntime GetCoreRuntime(Configs configs, IUmbracoVersion umbracoVersion, IIOHelper ioHelper, ILogger logger,
private static IRuntime GetCoreRuntime(
Configs configs, IUmbracoVersion umbracoVersion, IIOHelper ioHelper, Core.Logging.ILogger logger,
IProfiler profiler, Core.Hosting.IHostingEnvironment hostingEnvironment, IBackOfficeInfo backOfficeInfo,
ITypeFinder typeFinder)
ITypeFinder typeFinder, IRequestCache requestCache)
{
var connectionStringConfig = configs.ConnectionStrings()[Constants.System.UmbracoConnectionName];
var connectionStringConfig = configs.ConnectionStrings()[Core.Constants.System.UmbracoConnectionName];
var dbProviderFactoryCreator = new SqlServerDbProviderFactoryCreator(
connectionStringConfig?.ProviderName,
DbProviderFactories.GetFactory);
@@ -145,53 +182,92 @@ namespace Umbraco.Web.Common.Extensions
var mainDom = new MainDom(logger, mainDomLock);
var coreRuntime = new CoreRuntime(configs, umbracoVersion, ioHelper, logger, profiler, new AspNetCoreBootPermissionsChecker(),
hostingEnvironment, backOfficeInfo, dbProviderFactoryCreator, mainDom, typeFinder);
var coreRuntime = new CoreRuntime(
configs,
umbracoVersion,
ioHelper,
logger,
profiler,
new AspNetCoreBootPermissionsChecker(),
hostingEnvironment,
backOfficeInfo,
dbProviderFactoryCreator,
mainDom,
typeFinder,
requestCache);
return coreRuntime;
}
private static IServiceCollection CreateCompositionRoot(IServiceCollection services, IWebHostEnvironment webHostEnvironment,
out ILogger logger, out Configs configs, out IIOHelper ioHelper, out Core.Hosting.IHostingEnvironment hostingEnvironment,
out IBackOfficeInfo backOfficeInfo, out IProfiler profiler)
private static IServiceCollection CreateCompositionRoot(
IServiceCollection services,
Configs configs,
IWebHostEnvironment webHostEnvironment,
ILoggingConfiguration loggingConfiguration,
out Core.Logging.ILogger logger,
out IIOHelper ioHelper,
out Core.Hosting.IHostingEnvironment hostingEnvironment,
out IBackOfficeInfo backOfficeInfo,
out IProfiler profiler)
{
// TODO: We need to avoid this, surely there's a way? See ContainerTests.BuildServiceProvider_Before_Host_Is_Configured
var serviceProvider = services.BuildServiceProvider();
var httpContextAccessor = serviceProvider.GetRequiredService<IHttpContextAccessor>();
configs = serviceProvider.GetService<Configs>();
if (configs == null)
throw new InvalidOperationException($"Could not resolve type {typeof(Configs)} from the container, ensure {nameof(AddUmbracoConfiguration)} is called before calling {nameof(AddUmbracoCore)}");
var hostingSettings = configs.Hosting();
var coreDebug = configs.CoreDebug();
var globalSettings = configs.Global();
hostingEnvironment = new AspNetCoreHostingEnvironment(hostingSettings, webHostEnvironment);
ioHelper = new IOHelper(hostingEnvironment, globalSettings);
logger = SerilogLogger.CreateWithDefaultConfiguration(hostingEnvironment,
new AspNetCoreSessionManager(httpContextAccessor),
// TODO: We need to avoid this, surely there's a way? See ContainerTests.BuildServiceProvider_Before_Host_Is_Configured
() => services.BuildServiceProvider().GetService<IRequestCache>(), coreDebug, ioHelper,
new AspNetCoreMarchal());
logger = AddLogger(services, hostingEnvironment, loggingConfiguration);
backOfficeInfo = new AspNetCoreBackOfficeInfo(globalSettings);
profiler = GetWebProfiler(hostingEnvironment, httpContextAccessor);
profiler = GetWebProfiler(hostingEnvironment);
return services;
}
/// <summary>
/// Create and configure the logger
/// </summary>
/// <param name="hostingEnvironment"></param>
private static Core.Logging.ILogger AddLogger(IServiceCollection services, Core.Hosting.IHostingEnvironment hostingEnvironment, ILoggingConfiguration loggingConfiguration)
{
// Create a serilog logger
var logger = SerilogLogger.CreateWithDefaultConfiguration(hostingEnvironment, loggingConfiguration);
// Wire up all the bits that serilog needs. We need to use our own code since the Serilog ext methods don't cater to our needs since
// we don't want to use the global serilog `Log` object and we don't have our own ILogger implementation before the HostBuilder runs which
// is the only other option that these ext methods allow.
// I have created a PR to make this nicer https://github.com/serilog/serilog-extensions-hosting/pull/19 but we'll need to wait for that.
// Also see : https://github.com/serilog/serilog-extensions-hosting/blob/dev/src/Serilog.Extensions.Hosting/SerilogHostBuilderExtensions.cs
services.AddSingleton<ILoggerFactory>(services => new SerilogLoggerFactory(logger.SerilogLog, false));
// This won't (and shouldn't) take ownership of the logger.
services.AddSingleton(logger.SerilogLog);
// Registered to provide two services...
var diagnosticContext = new DiagnosticContext(logger.SerilogLog);
// Consumed by e.g. middleware
services.AddSingleton(diagnosticContext);
// Consumed by user code
services.AddSingleton<IDiagnosticContext>(diagnosticContext);
return logger;
}
public static IServiceCollection AddUmbracoRuntimeMinifier(this IServiceCollection services,
IConfiguration configuration)
{
services.AddSmidge(configuration.GetSection(Constants.Configuration.ConfigRuntimeMinification));
services.AddSmidge(configuration.GetSection(Core.Constants.Configuration.ConfigRuntimeMinification));
services.AddSmidgeNuglify();
return services;
}
private static IProfiler GetWebProfiler(Umbraco.Core.Hosting.IHostingEnvironment hostingEnvironment, IHttpContextAccessor httpContextAccessor)
private static IProfiler GetWebProfiler(Umbraco.Core.Hosting.IHostingEnvironment hostingEnvironment)
{
// create and start asap to profile boot
if (!hostingEnvironment.IsDebugMode)
@@ -201,7 +277,7 @@ namespace Umbraco.Web.Common.Extensions
return new VoidProfiler();
}
var webProfiler = new WebProfiler(httpContextAccessor);
var webProfiler = new WebProfiler();
webProfiler.StartBoot();
return webProfiler;
@@ -216,4 +292,5 @@ namespace Umbraco.Web.Common.Extensions
}
}
@@ -0,0 +1,35 @@
using System;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.Net.Http.Headers;
namespace Umbraco.Web.Common.Filters
{
/// <summary>
/// Ensures that the request is not cached by the browser
/// </summary>
public class DisableBrowserCacheAttribute : ActionFilterAttribute
{
public override void OnResultExecuting(ResultExecutingContext context)
{
base.OnResultExecuting(context);
var httpResponse = context.HttpContext.Response;
if (httpResponse.StatusCode != 200) return;
httpResponse.GetTypedHeaders().CacheControl =
new CacheControlHeaderValue()
{
NoCache = true,
MaxAge = TimeSpan.Zero,
MustRevalidate = true,
NoStore = true
};
httpResponse.Headers[HeaderNames.LastModified] = DateTime.Now.ToString("R"); // Format RFC1123
httpResponse.Headers[HeaderNames.Pragma] = "no-cache";
httpResponse.Headers[HeaderNames.Expires] = new DateTime(1990, 1, 1, 0, 0, 0).ToString("R");
}
}
}
@@ -0,0 +1,88 @@
using System.IO;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.AspNetCore.Mvc.ViewEngines;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using Umbraco.Web.Common.Constants;
using Umbraco.Web.Common.Controllers;
namespace Umbraco.Web.Common.Filters
{
/// <summary>
/// This is a special filter which is required for the RTE to be able to render Partial View Macros that
/// contain forms when the RTE value is resolved outside of an MVC view being rendered
/// </summary>
/// <remarks>
/// The entire way that we support partial view macros that contain forms isn't really great, these forms
/// need to be executed as ChildActions so that the ModelState,ViewData,TempData get merged into that action
/// so the form can show errors, viewdata, etc...
/// Under normal circumstances, macros will be rendered after a ViewContext is created but in some cases
/// developers will resolve the RTE value in the controller, in this case the Form won't be rendered correctly
/// with merged ModelState from the controller because the special DataToken hasn't been set yet (which is
/// normally done in the UmbracoViewPageOfModel when a real ViewContext is available.
/// So we need to detect if the currently rendering controller is IRenderController and if so we'll ensure that
/// this DataToken exists before the action executes in case the developer resolves an RTE value that contains
/// a partial view macro form.
/// </remarks>
public class EnsurePartialViewMacroViewContextFilterAttribute : ActionFilterAttribute
{
/// <summary>
/// Ensures the custom ViewContext datatoken is set before the RenderController action is invoked,
/// this ensures that any calls to GetPropertyValue with regards to RTE or Grid editors can still
/// render any PartialViewMacro with a form and maintain ModelState
/// </summary>
/// <param name="context"></param>
public override void OnActionExecuting(ActionExecutingContext context)
{
if (!(context.Controller is Controller controller)) return;
//ignore anything that is not IRenderController
if (!(controller is RenderController)) return;
SetViewContext(context, controller);
}
/// <summary>
/// Ensures that the custom ViewContext datatoken is set after the RenderController action is invoked,
/// this ensures that any custom ModelState that may have been added in the RenderController itself is
/// passed onwards in case it is required when rendering a PartialViewMacro with a form
/// </summary>
/// <param name="context">The filter context.</param>
public override void OnResultExecuting(ResultExecutingContext context)
{
if (!(context.Controller is Controller controller)) return;
//ignore anything that is not IRenderController
if (!(controller is RenderController)) return;
SetViewContext(context, controller);
}
private void SetViewContext(ActionContext context, Controller controller)
{
var viewCtx = new ViewContext(
context,
new DummyView(),
controller.ViewData,
controller.TempData,
new StringWriter(),
new HtmlHelperOptions());
//set the special data token
context.RouteData.DataTokens[ViewConstants.DataTokenCurrentViewContext] = viewCtx;
}
private class DummyView : IView
{
public Task RenderAsync(ViewContext context)
{
return Task.CompletedTask;
}
public string Path { get; }
}
}
}
@@ -0,0 +1,43 @@
using System;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
using Umbraco.Web.Common.Events;
namespace Umbraco.Web.Common.Filters
{
public class PreRenderViewActionFilterAttribute : ActionFilterAttribute
{
public override void OnActionExecuted(ActionExecutedContext context)
{
if (!(context.Controller is Controller umbController) || !(context.Result is ViewResult result))
{
return;
}
var model = result.Model;
if (model == null)
{
return;
}
var args = new ActionExecutedEventArgs(umbController, model);
OnActionExecuted(args);
if (args.Model != model)
{
result.ViewData.Model = args.Model;
}
base.OnActionExecuted(context);
}
public static event EventHandler<ActionExecutedEventArgs> ActionExecuted;
private static void OnActionExecuted(ActionExecutedEventArgs e)
{
var handler = ActionExecuted;
handler?.Invoke(null, e);
}
}
}
@@ -0,0 +1,39 @@
using System.Net;
using Microsoft.AspNetCore.Diagnostics;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.Extensions.DependencyInjection;
using Umbraco.Core.Configuration.UmbracoSettings;
namespace Umbraco.Web.Common.Filters
{
/// <summary>
/// Forces the response to have a specific http status code
/// </summary>
public class StatusCodeResultAttribute : ActionFilterAttribute
{
private readonly HttpStatusCode _statusCode;
public StatusCodeResultAttribute(HttpStatusCode statusCode)
{
_statusCode = statusCode;
}
public override void OnActionExecuted(ActionExecutedContext context)
{
base.OnActionExecuted(context);
var httpContext = context.HttpContext;
httpContext.Response.StatusCode = (int)_statusCode;
var disableIisCustomErrors = httpContext.RequestServices.GetService<IWebRoutingSettings>().TrySkipIisCustomErrors;
var statusCodePagesFeature = httpContext.Features.Get<IStatusCodePagesFeature>();
if (statusCodePagesFeature != null)
{
// if IIS Custom Errors are disabled, we won't enable the Status Code Pages
statusCodePagesFeature.Enabled = !disableIisCustomErrors;
}
}
}
}
@@ -0,0 +1,41 @@
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Serilog.Context;
using Umbraco.Core.Cache;
using Umbraco.Core.Logging.Serilog.Enrichers;
using Umbraco.Net;
namespace Umbraco.Web.Common.Middleware
{
public class UmbracoRequestLoggingMiddleware
{
readonly RequestDelegate _next;
private readonly HttpSessionIdEnricher _sessionIdEnricher;
private readonly HttpRequestNumberEnricher _requestNumberEnricher;
private readonly HttpRequestIdEnricher _requestIdEnricher;
public UmbracoRequestLoggingMiddleware(RequestDelegate next,
HttpSessionIdEnricher sessionIdEnricher,
HttpRequestNumberEnricher requestNumberEnricher,
HttpRequestIdEnricher requestIdEnricher)
{
_next = next;
_sessionIdEnricher = sessionIdEnricher;
_requestNumberEnricher = requestNumberEnricher;
_requestIdEnricher = requestIdEnricher;
}
public async Task Invoke(HttpContext httpContext)
{
// TODO: Need to decide if we want this stuff still, there's new request logging in serilog:
// https://github.com/serilog/serilog-aspnetcore#request-logging which i think would suffice and replace all of this?
using (LogContext.Push(_sessionIdEnricher))
using (LogContext.Push(_requestNumberEnricher))
using (LogContext.Push(_requestIdEnricher))
{
await _next(httpContext);
}
}
}
}
@@ -22,5 +22,4 @@ namespace Umbraco.Web.Common.Middleware
_umbracoRequestLifetimeManager.EndRequest(context);
}
}
}
@@ -7,6 +7,9 @@ using Umbraco.Core.Runtime;
using Umbraco.Core.Security;
using Umbraco.Web.Common.AspNetCore;
using Umbraco.Web.Common.Lifetime;
using Umbraco.Core.Diagnostics;
using Umbraco.Web.Common.Runtime.Profiler;
using Umbraco.Core.Logging;
namespace Umbraco.Web.Common.Runtime
{
@@ -42,6 +45,10 @@ namespace Umbraco.Web.Common.Runtime
composition.RegisterUnique<ICookieManager, AspNetCoreCookieManager>();
composition.RegisterMultipleUnique<ISessionIdResolver, ISessionManager, AspNetCoreSessionManager>();
composition.RegisterUnique<IMarchal, AspNetCoreMarchal>();
composition.RegisterUnique<IProfilerHtml, WebProfilerHtml>();
}
}
}
@@ -1,61 +1,21 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Extensions;
using StackExchange.Profiling;
using StackExchange.Profiling.Internal;
using Umbraco.Core;
using Umbraco.Core.Logging;
// TODO: This namespace is strange, not sure why i has "Runtime" in the name?
namespace Umbraco.Web.Common.Runtime.Profiler
{
public class WebProfiler : IProfiler
{
private MiniProfiler _startupProfiler;
private readonly IHttpContextAccessor _httpContextAccessor;
private int _first;
public WebProfiler(IHttpContextAccessor httpContextAccessor)
{
// create our own provider, which can provide a profiler even during boot
_httpContextAccessor = httpContextAccessor;
}
/// <summary>
///
/// </summary>
/// <remarks>
/// Normally we would call MiniProfiler.Current.RenderIncludes(...), but because the requeststate is not set, this method does not work.
/// We fake the requestIds from the RequestState here.
/// </remarks>
public string Render()
{
var profiler = MiniProfiler.Current;
if (profiler == null) return string.Empty;
var context = _httpContextAccessor.HttpContext;
var path = (profiler.Options as MiniProfilerOptions)?.RouteBasePath.Value.EnsureTrailingSlash();
var result = StackExchange.Profiling.Internal.Render.Includes(
profiler,
path: context.Request.PathBase + path,
isAuthorized: true,
requestIDs: new List<Guid>{ profiler.Id },
position: RenderPosition.Right,
showTrivial: profiler.Options.PopupShowTrivial,
showTimeWithChildren: profiler.Options.PopupShowTimeWithChildren,
maxTracesToShow: profiler.Options.PopupMaxTracesToShow,
showControls:profiler.Options.ShowControls,
startHidden: profiler.Options.PopupStartHidden);
return result;
}
public IDisposable Step(string name)
{
return MiniProfiler.Current?.Step(name);
@@ -1,8 +1,15 @@
using Umbraco.Core.Composing;
using Umbraco.Core;
using Umbraco.Core.Composing;
namespace Umbraco.Web.Common.Runtime.Profiler
{
internal class WebProfilerComposer : ComponentComposer<WebProfilerComponent>, ICoreComposer
{
public override void Compose(Composition composition)
{
base.Compose(composition);
composition.RegisterUnique<WebProfilerHtml>();
}
}
}
@@ -0,0 +1,51 @@
using System;
using System.Collections.Generic;
using Microsoft.AspNetCore.Http;
using StackExchange.Profiling;
using StackExchange.Profiling.Internal;
using Umbraco.Core.Logging;
// TODO: This namespace is strange, not sure why i has "Runtime" in the name?
namespace Umbraco.Web.Common.Runtime.Profiler
{
public class WebProfilerHtml : IProfilerHtml
{
private readonly IHttpContextAccessor _httpContextAccessor;
public WebProfilerHtml(IHttpContextAccessor httpContextAccessor)
{
// create our own provider, which can provide a profiler even during boot
_httpContextAccessor = httpContextAccessor;
}
/// <inheritdoc />
/// <remarks>
/// Normally we would call MiniProfiler.Current.RenderIncludes(...), but because the requeststate is not set, this method does not work.
/// We fake the requestIds from the RequestState here.
/// </remarks>
public string Render()
{
var profiler = MiniProfiler.Current;
if (profiler == null) return string.Empty;
var context = _httpContextAccessor.HttpContext;
var path = (profiler.Options as MiniProfilerOptions)?.RouteBasePath.Value.EnsureTrailingSlash();
var result = StackExchange.Profiling.Internal.Render.Includes(
profiler,
path: context.Request.PathBase + path,
isAuthorized: true,
requestIDs: new List<Guid> { profiler.Id },
position: RenderPosition.Right,
showTrivial: profiler.Options.PopupShowTrivial,
showTimeWithChildren: profiler.Options.PopupShowTimeWithChildren,
maxTracesToShow: profiler.Options.PopupMaxTracesToShow,
showControls: profiler.Options.ShowControls,
startHidden: profiler.Options.PopupStartHidden);
return result;
}
}
}
@@ -112,7 +112,7 @@ namespace Umbraco.Web.Common.RuntimeMinification
public void Reset()
{
var version = DateTime.UtcNow.Ticks.ToString();
_configManipulator.SaveConfigValue(Constants.Configuration.ConfigRuntimeMinificationVersion, version.ToString());
_configManipulator.SaveConfigValue(Core.Constants.Configuration.ConfigRuntimeMinificationVersion, version.ToString());
}
@@ -18,6 +18,7 @@
<ItemGroup>
<PackageReference Include="MiniProfiler.AspNetCore.Mvc" Version="4.1.0" />
<PackageReference Include="Serilog.AspNetCore" Version="3.2.0" />
<PackageReference Include="Smidge" Version="3.1.0" />
<PackageReference Include="Smidge.Nuglify" Version="2.0.0" />
</ItemGroup>
@@ -0,0 +1,42 @@
[
{
"name": "Find all logs where the Level is NOT Verbose and NOT Debug",
"query": "Not(@Level='Verbose') and Not(@Level='Debug')"
},
{
"name": "Find all logs that has an exception property (Warning, Error & Fatal with Exceptions)",
"query": "Has(@Exception)"
},
{
"name": "Find all logs that have the property 'Duration'",
"query": "Has(Duration)"
},
{
"name": "Find all logs that have the property 'Duration' and the duration is greater than 1000ms",
"query": "Has(Duration) and Duration > 1000"
},
{
"name": "Find all logs that are from the namespace 'Umbraco.Core'",
"query": "StartsWith(SourceContext, 'Umbraco.Core')"
},
{
"name": "Find all logs that use a specific log message template",
"query": "@MessageTemplate = '[Timing {TimingId}] {EndMessage} ({TimingDuration}ms)'"
},
{
"name": "Find logs where one of the items in the SortedComponentTypes property array is equal to",
"query": "SortedComponentTypes[?] = 'Umbraco.Web.Search.ExamineComponent'"
},
{
"name": "Find logs where one of the items in the SortedComponentTypes property array contains",
"query": "Contains(SortedComponentTypes[?], 'DatabaseServer')"
},
{
"name": "Find all logs that the message has localhost in it with SQL like",
"query": "@Message like '%localhost%'"
},
{
"name": "Find all logs that the message that starts with 'end' in it with SQL like",
"query": "@Message like 'end%'"
}
]
@@ -0,0 +1,41 @@
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<appSettings>
<!-- Used to toggle the log levels for the main Umbraco log files -->
<!-- Found at /app_data/logs/ -->
<!-- NOTE: Changing this will also flow down into serilog.user.config -->
<!-- VALID Values: Verbose, Debug, Information, Warning, Error, Fatal -->
<add key="serilog:minimum-level" value="Information" />
<!-- NOTE: This is how sources can have a different level -->
<!--
<add key="serilog:minimum-level:override:Umbraco.Core.Composing.TypeLoader" value="Warning" />
-->
<!-- NOTE: Only one logger below can be enabled, you cannot log JSON & TXT files at the same time -->
<!-- Default JSON log file -->
<!-- This is used by the default log viewer in the Umbraco backoffice -->
<add key="serilog:using:File" value="Umbraco.Infrastructure" />
<add key="serilog:write-to:File.formatter" value="Serilog.Formatting.Compact.CompactJsonFormatter, Serilog.Formatting.Compact" />
<add key="serilog:write-to:File.path" value="%UMBLOGDIR%\UmbracoTraceLog.%MACHINENAME%..json" />
<add key="serilog:write-to:File.restrictedToMinimumLevel" value="Debug" />
<add key="serilog:write-to:File.retainedFileCountLimit" value="" /> <!-- Number of log files to keep (or remove value to keep all files) -->
<add key="serilog:write-to:File.rollingInterval" value="Day" /> <!-- Create a new log file every Minute/Hour/Day/Month/Year/infinite -->
<!-- Optional TXT log file -->
<!--<add key="serilog:using:File" value="Serilog.Sinks.File" /> -->
<!--<add key="serilog:write-to:File.path" value="%UMBLOGDIR%\UmbracoTraceLog.%MACHINENAME%..txt" /> -->
<!--<add key="serilog:write-to:File.shared" value="true" /> -->
<!--<add key="serilog:write-to:File.restrictedToMinimumLevel" value="Debug" /> -->
<!--<add key="serilog:write-to:File.retainedFileCountLimit" value="" /> --> <!-- Number of log files to keep (or remove value to keep all files) -->
<!--<add key="serilog:write-to:File.rollingInterval" value="Day" /> --> <!-- Create a new log file every Minute/Hour/Day/Month/Year/infinite -->
<!--<add key="serilog:write-to:File.outputTemplate" value="{Timestamp:yyyy-MM-dd HH:mm:ss,fff} [P{ProcessId}/D{AppDomainId}/T{ThreadId}] {Log4NetLevel} {SourceContext} - {Message:lj}{NewLine}{Exception}" /> -->
<!-- To write to new log locations (aka Sinks) such as your own .txt files with filtering, ELMAH.io, Elastic, SEQ -->
<!-- Please use the serilog.user.config file to configure your own logging needs -->
</appSettings>
</configuration>
@@ -0,0 +1,41 @@
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<appSettings>
<!-- Used to toggle the log levels for the main Umbraco log files -->
<!-- Found at /app_data/logs/ -->
<!-- NOTE: Changing this will also flow down into serilog.user.config -->
<!-- VALID Values: Verbose, Debug, Information, Warning, Error, Fatal -->
<add key="serilog:minimum-level" value="Information" />
<!-- NOTE: This is how sources can have a different level -->
<!--
<add key="serilog:minimum-level:override:Umbraco.Core.Composing.TypeLoader" value="Warning" />
-->
<!-- NOTE: Only one logger below can be enabled, you cannot log JSON & TXT files at the same time -->
<!-- Default JSON log file -->
<!-- This is used by the default log viewer in the Umbraco backoffice -->
<add key="serilog:using:File" value="Umbraco.Infrastructure" />
<add key="serilog:write-to:File.formatter" value="Serilog.Formatting.Compact.CompactJsonFormatter, Serilog.Formatting.Compact" />
<add key="serilog:write-to:File.path" value="%UMBLOGDIR%\UmbracoTraceLog.%MACHINENAME%..json" />
<add key="serilog:write-to:File.restrictedToMinimumLevel" value="Debug" />
<add key="serilog:write-to:File.retainedFileCountLimit" value="" /> <!-- Number of log files to keep (or remove value to keep all files) -->
<add key="serilog:write-to:File.rollingInterval" value="Day" /> <!-- Create a new log file every Minute/Hour/Day/Month/Year/infinite -->
<!-- Optional TXT log file -->
<!--<add key="serilog:using:File" value="Serilog.Sinks.File" /> -->
<!--<add key="serilog:write-to:File.path" value="%UMBLOGDIR%\UmbracoTraceLog.%MACHINENAME%..txt" /> -->
<!--<add key="serilog:write-to:File.shared" value="true" /> -->
<!--<add key="serilog:write-to:File.restrictedToMinimumLevel" value="Debug" /> -->
<!--<add key="serilog:write-to:File.retainedFileCountLimit" value="" /> --> <!-- Number of log files to keep (or remove value to keep all files) -->
<!--<add key="serilog:write-to:File.rollingInterval" value="Day" /> --> <!-- Create a new log file every Minute/Hour/Day/Month/Year/infinite -->
<!--<add key="serilog:write-to:File.outputTemplate" value="{Timestamp:yyyy-MM-dd HH:mm:ss,fff} [P{ProcessId}/D{AppDomainId}/T{ThreadId}] {Log4NetLevel} {SourceContext} - {Message:lj}{NewLine}{Exception}" /> -->
<!-- To write to new log locations (aka Sinks) such as your own .txt files with filtering, ELMAH.io, Elastic, SEQ -->
<!-- Please use the serilog.user.config file to configure your own logging needs -->
</appSettings>
</configuration>
@@ -0,0 +1,39 @@
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<appSettings>
<!-- Controls log levels for all user-defined child sub-logger sinks configured here (Set this higher than child sinks defined here) -->
<!-- VALID Values: Verbose, Debug, Information, Warning, Error, Fatal -->
<add key="serilog:minimum-level" value="Information" />
<!-- For Different Namespaces - Set different logging levels -->
<!--
<add key="serilog:minimum-level:override:Microsoft" value="Warning" />
<add key="serilog:minimum-level:override:Microsoft.AspNetCore.Mvc" value="Error" />
<add key="serilog:minimum-level:override:YourNameSpace" value="Information" />
-->
<!-- All logs defined via user.config will contain this property (won't be in main Umbraco logs) -->
<!--
<add key="serilog:enrich:with-property:websiteName" value="My Awesome Website - Development" />
-->
<!-- Write to a user log file -->
<!--
<add key="serilog:using:File" value="Serilog.Sinks.File" />
<add key="serilog:write-to:File.path" value="%BASEDIR%\logs\my-custom-logfile.txt" />
<add key="serilog:write-to:File.shared" value="true" />
<add key="serilog:write-to:File.restrictedToMinimumLevel" value="Debug" />
<add key="serilog:write-to:File.retainedFileCountLimit" value="32" />--> <!-- Number of log files to keep (or remove value to keep all files) -->
<!--<add key="serilog:write-to:File.rollingInterval" value="Day" />--> <!-- Create a new log file every Minute/Hour/Day/Month/Year/infinite -->
<!--<add key="serilog:write-to:File.outputTemplate" value="{Timestamp:yyyy-MM-dd HH:mm:ss,fff} [P{ProcessId}/D{AppDomainId}/T{ThreadId}] {Log4NetLevel} {SourceContext} - {Message:lj}{NewLine}{Exception}" /> -->
<!-- Filters all above sink's to use this expression -->
<!-- Common use case is to include SourceType starting with your own namespace -->
<!--
<add key="serilog:using:FilterExpressions" value="Serilog.Filters.Expressions" />
<add key="serilog:filter:ByIncludingOnly.expression" value="StartsWith(SourceContext, 'Umbraco.Core')" />
-->
</appSettings>
</configuration>
@@ -0,0 +1,39 @@
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<appSettings>
<!-- Controls log levels for all user-defined child sub-logger sinks configured here (Set this higher than child sinks defined here) -->
<!-- VALID Values: Verbose, Debug, Information, Warning, Error, Fatal -->
<add key="serilog:minimum-level" value="Information" />
<!-- For Different Namespaces - Set different logging levels -->
<!--
<add key="serilog:minimum-level:override:Microsoft" value="Warning" />
<add key="serilog:minimum-level:override:Microsoft.AspNetCore.Mvc" value="Error" />
<add key="serilog:minimum-level:override:YourNameSpace" value="Information" />
-->
<!-- All logs defined via user.config will contain this property (won't be in main Umbraco logs) -->
<!--
<add key="serilog:enrich:with-property:websiteName" value="My Awesome Website - Development" />
-->
<!-- Write to a user log file -->
<!--
<add key="serilog:using:File" value="Serilog.Sinks.File" />
<add key="serilog:write-to:File.path" value="%BASEDIR%\logs\my-custom-logfile.txt" />
<add key="serilog:write-to:File.shared" value="true" />
<add key="serilog:write-to:File.restrictedToMinimumLevel" value="Debug" />
<add key="serilog:write-to:File.retainedFileCountLimit" value="32" />--> <!-- Number of log files to keep (or remove value to keep all files) -->
<!--<add key="serilog:write-to:File.rollingInterval" value="Day" />--> <!-- Create a new log file every Minute/Hour/Day/Month/Year/infinite -->
<!--<add key="serilog:write-to:File.outputTemplate" value="{Timestamp:yyyy-MM-dd HH:mm:ss,fff} [P{ProcessId}/D{AppDomainId}/T{ThreadId}] {Log4NetLevel} {SourceContext} - {Message:lj}{NewLine}{Exception}" /> -->
<!-- Filters all above sink's to use this expression -->
<!-- Common use case is to include SourceType starting with your own namespace -->
<!--
<add key="serilog:using:FilterExpressions" value="Serilog.Filters.Expressions" />
<add key="serilog:filter:ByIncludingOnly.expression" value="StartsWith(SourceContext, 'Umbraco.Core')" />
-->
</appSettings>
</configuration>
-2
View File
@@ -1,7 +1,5 @@
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Serilog;
using Umbraco.Core.Composing;
namespace Umbraco.Web.UI.BackOffice
+5 -1
View File
@@ -54,6 +54,7 @@ namespace Umbraco.Web.UI.BackOffice
});
//Finally initialize Current
// TODO: This should be moved to the UmbracoServiceProviderFactory when the container is cross-wired and then don't use the overload above to `out var factory`
Current.Initialize(
factory.GetInstance<ILogger> (),
factory.GetInstance<Configs>(),
@@ -75,7 +76,9 @@ namespace Umbraco.Web.UI.BackOffice
{
app.UseDeveloperExceptionPage();
}
app.UseStatusCodePages();
app.UseUmbracoCore();
app.UseUmbracoRequestLogging();
app.UseUmbracoWebsite();
app.UseUmbracoBackOffice();
app.UseRouting();
@@ -89,7 +92,8 @@ namespace Umbraco.Web.UI.BackOffice
});
endpoints.MapGet("/", async context =>
{
await context.Response.WriteAsync($"<html><body>Hello World!{Current.Profiler.Render()}</body></html>");
var profilerHtml = app.ApplicationServices.GetRequiredService<IProfilerHtml>();
await context.Response.WriteAsync($"<html><body>Hello World!{profilerHtml.Render()}</body></html>");
});
});
}
@@ -35,4 +35,37 @@
<Content Remove="App_Data\**" />
</ItemGroup>
<ItemGroup>
<Content Remove="Config\serilog.Release.config" />
<Content Remove="Config\serilog.user.Release.config" />
</ItemGroup>
<ItemGroup>
<None Remove="Config\logviewer.searches.config.js" />
</ItemGroup>
<ItemGroup>
<Content Include="Config\logviewer.searches.config.js" />
</ItemGroup>
<ItemGroup>
<None Include="Config\serilog.Release.config">
<SubType>Designer</SubType>
<DependentUpon>serilog.config</DependentUpon>
</None>
<None Include="Config\serilog.user.Release.config">
<SubType>Designer</SubType>
<DependentUpon>serilog.user.config</DependentUpon>
</None>
</ItemGroup>
<ItemGroup>
<Content Update="Config\serilog.config">
<SubType>Designer</SubType>
</Content>
<Content Update="Config\serilog.user.config">
<SubType>Designer</SubType>
</Content>
</ItemGroup>
</Project>
+1
View File
@@ -345,6 +345,7 @@
<DevelopmentServerPort>8610</DevelopmentServerPort>
<DevelopmentServerVPath>/</DevelopmentServerVPath>
<IISUrl>http://localhost:8610</IISUrl>
<IISUrl>http://localhost:8700</IISUrl>
<NTLMAuthentication>False</NTLMAuthentication>
<UseCustomServer>False</UseCustomServer>
<CustomServerUrl>
@@ -19,7 +19,7 @@
<!-- This is used by the default log viewer in the Umbraco backoffice -->
<add key="serilog:using:File" value="Umbraco.Infrastructure" />
<add key="serilog:write-to:File.formatter" value="Serilog.Formatting.Compact.CompactJsonFormatter, Serilog.Formatting.Compact" />
<add key="serilog:write-to:File.path" value="%BASEDIR%\App_Data\Logs\UmbracoTraceLog.%MACHINENAME%..json" />
<add key="serilog:write-to:File.path" value="%UMBLOGDIR%\UmbracoTraceLog.%MACHINENAME%..json" />
<add key="serilog:write-to:File.restrictedToMinimumLevel" value="Debug" />
<add key="serilog:write-to:File.retainedFileCountLimit" value="" /> <!-- Number of log files to keep (or remove value to keep all files) -->
<add key="serilog:write-to:File.rollingInterval" value="Day" /> <!-- Create a new log file every Minute/Hour/Day/Month/Year/infinite -->
@@ -27,7 +27,7 @@
<!-- Optional TXT log file -->
<!--<add key="serilog:using:File" value="Serilog.Sinks.File" /> -->
<!--<add key="serilog:write-to:File.path" value="%BASEDIR%\App_Data\Logs\UmbracoTraceLog.%MACHINENAME%..txt" /> -->
<!--<add key="serilog:write-to:File.path" value="%UMBLOGDIR%\UmbracoTraceLog.%MACHINENAME%..txt" /> -->
<!--<add key="serilog:write-to:File.shared" value="true" /> -->
<!--<add key="serilog:write-to:File.restrictedToMinimumLevel" value="Debug" /> -->
<!--<add key="serilog:write-to:File.retainedFileCountLimit" value="" /> --> <!-- Number of log files to keep (or remove value to keep all files) -->
+2
View File
@@ -248,6 +248,8 @@ namespace Umbraco.Web.Composing
public static IProfiler Profiler => Factory.GetInstance<IProfiler>();
public static IProfilerHtml ProfilerHtml => Factory.GetInstance<IProfilerHtml>();
public static IProfilingLogger ProfilingLogger => Factory.GetInstance<IProfilingLogger>();
public static AppCaches AppCaches => Factory.GetInstance<AppCaches>();
@@ -0,0 +1,124 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Serilog.Core;
using Umbraco.Core.Models;
using Umbraco.Core.Services;
namespace Umbraco.Web.HealthCheck.Checks.Data
{
[HealthCheck(
"73DD0C1C-E0CA-4C31-9564-1DCA509788AF",
"Database data integrity check",
Description = "Checks for various data integrity issues in the Umbraco database.",
Group = "Data Integrity")]
public class DatabaseIntegrityCheck : HealthCheck
{
private readonly IContentService _contentService;
private readonly IMediaService _mediaService;
private const string _fixMediaPaths = "fixMediaPaths";
private const string _fixContentPaths = "fixContentPaths";
private const string _fixMediaPathsTitle = "Fix media paths";
private const string _fixContentPathsTitle = "Fix content paths";
public DatabaseIntegrityCheck(IContentService contentService, IMediaService mediaService)
{
_contentService = contentService;
_mediaService = mediaService;
}
/// <summary>
/// Get the status for this health check
/// </summary>
/// <returns></returns>
public override IEnumerable<HealthCheckStatus> GetStatus()
{
//return the statuses
return new[]
{
CheckDocuments(false),
CheckMedia(false)
};
}
private HealthCheckStatus CheckMedia(bool fix)
{
return CheckPaths(_fixMediaPaths, _fixMediaPathsTitle, Core.Constants.UdiEntityType.Media, fix,
() => _mediaService.CheckDataIntegrity(new ContentDataIntegrityReportOptions {FixIssues = fix}));
}
private HealthCheckStatus CheckDocuments(bool fix)
{
return CheckPaths(_fixContentPaths, _fixContentPathsTitle, Core.Constants.UdiEntityType.Document, fix,
() => _contentService.CheckDataIntegrity(new ContentDataIntegrityReportOptions {FixIssues = fix}));
}
private HealthCheckStatus CheckPaths(string actionAlias, string actionName, string entityType, bool detailedReport, Func<ContentDataIntegrityReport> doCheck)
{
var report = doCheck();
var actions = new List<HealthCheckAction>();
if (!report.Ok)
{
actions.Add(new HealthCheckAction(actionAlias, Id)
{
Name = actionName
});
}
return new HealthCheckStatus(GetReport(report, entityType, detailedReport))
{
ResultType = report.Ok ? StatusResultType.Success : StatusResultType.Error,
Actions = actions
};
}
private static string GetReport(ContentDataIntegrityReport report, string entityType, bool detailed)
{
var sb = new StringBuilder();
if (report.Ok)
{
sb.AppendLine($"<p>All {entityType} paths are valid</p>");
if (!detailed)
return sb.ToString();
}
else
{
sb.AppendLine($"<p>{report.DetectedIssues.Count} invalid {entityType} paths detected.</p>");
}
if (detailed && report.DetectedIssues.Count > 0)
{
sb.AppendLine("<ul>");
foreach (var issueGroup in report.DetectedIssues.GroupBy(x => x.Value.IssueType))
{
var countByGroup = issueGroup.Count();
var fixedByGroup = issueGroup.Count(x => x.Value.Fixed);
sb.AppendLine("<li>");
sb.AppendLine($"{countByGroup} issues of type <code>{issueGroup.Key}</code> ... {fixedByGroup} fixed");
sb.AppendLine("</li>");
}
sb.AppendLine("</ul>");
}
return sb.ToString();
}
public override HealthCheckStatus ExecuteAction(HealthCheckAction action)
{
switch (action.Alias)
{
case _fixContentPaths:
return CheckDocuments(true);
case _fixMediaPaths:
return CheckMedia(true);
default:
throw new InvalidOperationException("Action not supported");
}
}
}
}
@@ -30,7 +30,7 @@ namespace Umbraco.Web
/// <returns></returns>
public static IHtmlString RenderProfiler(this HtmlHelper helper)
{
return new HtmlString(Current.Profiler.Render());
return new HtmlString(Current.ProfilerHtml.Render());
}
/// <summary>
+1 -1
View File
@@ -14,7 +14,7 @@ namespace Umbraco.Web.Logging
/// <remarks>
/// Profiling only runs when the app is in debug mode, see WebRuntime for how this gets created
/// </remarks>
internal class WebProfiler : IProfiler
internal class WebProfiler : IProfiler, IProfilerHtml
{
private const string BootRequestItemKey = "Umbraco.Core.Logging.WebProfiler__isBootRequest";
private readonly WebProfilerProvider _provider;
@@ -3,6 +3,7 @@ using System.Web.Mvc;
namespace Umbraco.Web.Mvc
{
/// Migrated already to .Net Core
public class ActionExecutedEventArgs : EventArgs
{
public Controller Controller { get; set; }
+1
View File
@@ -3,6 +3,7 @@
/// <summary>
/// constants
/// </summary>
/// Migrated already to .Net Core
internal static class Constants
{
internal const string ViewLocation = "~/Views";
@@ -19,6 +19,7 @@ namespace Umbraco.Web.Mvc
/// this DataToken exists before the action executes in case the developer resolves an RTE value that contains
/// a partial view macro form.
/// </remarks>
/// Migrated already to .Net Core
internal class EnsurePartialViewMacroViewContextFilterAttribute : ActionFilterAttribute
{
/// <summary>
+1
View File
@@ -5,6 +5,7 @@ namespace Umbraco.Web.Mvc
/// <summary>
/// A marker interface to designate that a controller will be used for Umbraco front-end requests and/or route hijacking
/// </summary>
/// Migrated already to .Net Core
public interface IRenderController : IController
{
@@ -12,6 +12,7 @@ namespace Umbraco.Web.Mvc
/// <remarks>
/// Only minifies in release mode
/// </remarks>
/// Migrated already to .Net Core
public class MinifyJavaScriptResultAttribute : ActionFilterAttribute
{
private readonly IHostingEnvironment _hostingEnvironment;
@@ -3,6 +3,7 @@ using System.Web.Mvc;
namespace Umbraco.Web.Mvc
{
/// Migrated already to .Net Core
public class PreRenderViewActionFilterAttribute : ActionFilterAttribute
{
public override void OnActionExecuted(ActionExecutedContext filterContext)
@@ -8,6 +8,7 @@ namespace Umbraco.Web.Mvc
/// <summary>
/// Forces the response to have a specific http status code
/// </summary>
/// Migrated already to .Net Core
internal class StatusCodeResultAttribute : ActionFilterAttribute
{
private readonly HttpStatusCode _statusCode;
@@ -25,6 +25,9 @@ using Umbraco.Web.Trees;
using Umbraco.Web.WebApi;
using Umbraco.Net;
using Umbraco.Web.AspNet;
using Umbraco.Core.Diagnostics;
using Umbraco.Core.Logging;
using Umbraco.Web.Logging;
namespace Umbraco.Web.Runtime
{
@@ -50,7 +53,8 @@ namespace Umbraco.Web.Runtime
composition.Register<IFilePermissionHelper, FilePermissionHelper>(Lifetime.Singleton);
composition.RegisterUnique<IMarchal, FrameworkMarchal>();
composition.RegisterUnique<IProfilerHtml, WebProfiler>();
composition.ComposeWebMappingProfiles();
+1
View File
@@ -164,6 +164,7 @@
<Compile Include="Editors\TinyMceController.cs" />
<Compile Include="HttpContextAccessorExtensions.cs" />
<Compile Include="HttpContextExtensions.cs" />
<Compile Include="HealthCheck\Checks\Data\DatabaseIntegrityCheck.cs" />
<Compile Include="ImageCropperTemplateCoreExtensions.cs" />
<Compile Include="Install\ChangesMonitor.cs" />
<Compile Include="Logging\OwinLogger.cs" />

Some files were not shown because too many files have changed in this diff Show More