Merge remote-tracking branch 'Umbraco/netcore/dev' into netcore/feature/users
This commit is contained in:
@@ -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).
|
||||
|
||||
|
||||
@@ -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";
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
@@ -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("[]");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+11
-11
@@ -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;
|
||||
+13
-73
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
+125
-8
@@ -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 }));
|
||||
|
||||
+158
-141
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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>
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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) -->
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user