diff --git a/src/Umbraco.Core/CoreRuntime.cs b/src/Umbraco.Core/CoreRuntime.cs index 12fa479f92..f62d7be387 100644 --- a/src/Umbraco.Core/CoreRuntime.cs +++ b/src/Umbraco.Core/CoreRuntime.cs @@ -115,6 +115,12 @@ namespace Umbraco.Core // throw a BootFailedException for every requests. } } + + // after Umbraco has started there is a scope in "context" and that context is + // going to stay there and never get destroyed nor reused, so we have to ensure that + // everything is cleared + var sa = container.GetInstance(); + sa.Scope?.Dispose(); } private void AquireMainDom(IServiceFactory container) @@ -218,7 +224,7 @@ namespace Umbraco.Core // will be initialized with syntax providers and a logger, and will try to configure // from the default connection string name, if possible, else will remain non-configured // until the database context configures it properly (eg when installing) - container.RegisterSingleton(); + container.RegisterSingleton(); // register database context container.RegisterSingleton(); diff --git a/src/Umbraco.Core/DatabaseContext.cs b/src/Umbraco.Core/DatabaseContext.cs index e587e857f4..f5c8392bf3 100644 --- a/src/Umbraco.Core/DatabaseContext.cs +++ b/src/Umbraco.Core/DatabaseContext.cs @@ -1,7 +1,5 @@ using System; using NPoco; -using Umbraco.Core.DI; -using Umbraco.Core.Logging; using Umbraco.Core.Persistence; using Umbraco.Core.Persistence.Querying; using Umbraco.Core.Persistence.SqlSyntax; @@ -20,7 +18,6 @@ namespace Umbraco.Core public class DatabaseContext { private readonly IDatabaseFactory _databaseFactory; - private bool _canConnectOnce; /// /// Initializes a new instance of the class. @@ -46,7 +43,7 @@ namespace Umbraco.Core public IQueryFactory QueryFactory => _databaseFactory.QueryFactory; /// - /// Gets the database sql syntax. + /// Gets the database Sql syntax. /// public ISqlSyntaxProvider SqlSyntax => _databaseFactory.SqlSyntax; @@ -66,11 +63,34 @@ namespace Umbraco.Core public IQuery Query() => _databaseFactory.QueryFactory.Create(); /// - /// Gets an "ambient" database for doing CRUD operations against custom tables that resides in the Umbraco database. + /// Gets an ambient database for doing CRUD operations against custom tables that resides in the Umbraco database. /// /// Should not be used for operation against standard Umbraco tables; as services should be used instead. public UmbracoDatabase Database => _databaseFactory.GetDatabase(); + /// + /// Gets an ambient database scope. + /// + /// A disposable object representing the scope. + public IDisposable CreateDatabaseScope() // fixme - move over to factory + { + var factory = _databaseFactory as UmbracoDatabaseFactory; // fixme - though... IDatabaseFactory? + if (factory == null) throw new NotSupportedException(); + return factory.CreateScope(); + } + +#if DEBUG_DATABASES + public List Databases + { + get + { + var factory = _databaseFactory as UmbracoDatabaseFactory; + if (factory == null) throw new NotSupportedException(); + return factory.Databases; + } + } +#endif + /// /// Gets a value indicating whether the database is configured. /// @@ -81,24 +101,6 @@ namespace Umbraco.Core /// /// Gets a value indicating whether it is possible to connect to the database. /// - public bool CanConnect - { - get - { - var canConnect = _databaseFactory.Configured && _databaseFactory.CanConnect; - - if (_canConnectOnce) - { - Current.Logger.Debug("CanConnect: " + canConnect); - } - else - { - Current.Logger.Info("CanConnect: " + canConnect); - _canConnectOnce = canConnect; // keep logging Info until we can connect - } - - return canConnect; - } - } + public bool CanConnect => _databaseFactory.Configured && _databaseFactory.CanConnect; } } \ No newline at end of file diff --git a/src/Umbraco.Core/Events/EventExtensions.cs b/src/Umbraco.Core/Events/EventExtensions.cs index 700a02457a..bd66b605fd 100644 --- a/src/Umbraco.Core/Events/EventExtensions.cs +++ b/src/Umbraco.Core/Events/EventExtensions.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Reflection; namespace Umbraco.Core.Events { @@ -47,5 +48,35 @@ namespace Umbraco.Core.Events if (eventHandler != null) eventHandler(sender, args); } - } + + // moves the last handler that was added to an instance event, to first position + public static void PromoteLastHandler(object sender, string eventName) + { + var fieldInfo = sender.GetType().GetField(eventName, BindingFlags.Instance | BindingFlags.NonPublic); + if (fieldInfo == null) throw new InvalidOperationException("No event named " + eventName + "."); + PromoteLastHandler(sender, fieldInfo); + } + + // moves the last handler that was added to a static event, to first position + public static void PromoteLastHandler(string eventName) + { + var fieldInfo = typeof(TSender).GetField(eventName, BindingFlags.Static | BindingFlags.NonPublic); + if (fieldInfo == null) throw new InvalidOperationException("No event named " + eventName + "."); + PromoteLastHandler(null, fieldInfo); + } + + private static void PromoteLastHandler(object sender, FieldInfo fieldInfo) + { + var d = fieldInfo.GetValue(sender) as Delegate; + if (d == null) return; + + var l = d.GetInvocationList(); + var x = l[l.Length - 1]; + for (var i = l.Length - 1; i > 0; i--) + l[i] = l[i - 1]; + l[0] = x; + + fieldInfo.SetValue(sender, Delegate.Combine(l)); + } + } } \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/DatabaseDebugHelper.cs b/src/Umbraco.Core/Persistence/DatabaseDebugHelper.cs new file mode 100644 index 0000000000..0bbd6f5bd5 --- /dev/null +++ b/src/Umbraco.Core/Persistence/DatabaseDebugHelper.cs @@ -0,0 +1,172 @@ +#if DEBUG_DATABASES +using System; +using System.Collections.Generic; +using System.Data; +using System.Data.Common; +using System.Linq; +using System.Reflection; +using System.Text; + +namespace Umbraco.Core.Persistence +{ + internal static class DatabaseDebugHelper + { + private const int CommandsSize = 100; + private static readonly Queue>> Commands = new Queue>>(); + + public static void SetCommand(IDbCommand command, string context) + { + command = command.UnwrapUmbraco(); + + lock (Commands) + { + Commands.Enqueue(Tuple.Create(context, new WeakReference(command))); + while (Commands.Count > CommandsSize) Commands.Dequeue(); + } + } + + public static string GetCommandContext(IDbCommand command) + { + lock (Commands) + { + var tuple = Commands.FirstOrDefault(x => + { + IDbCommand c; + return x.Item2.TryGetTarget(out c) && c == command; + }); + return tuple == null ? "?" : tuple.Item1; + } + } + + public static string GetReferencedObjects(IDbConnection con) + { + con = con.UnwrapUmbraco(); + + var ceCon = con as System.Data.SqlServerCe.SqlCeConnection; + if (ceCon != null) return null; // "NotSupported: SqlCE"; + + var dbCon = con as DbConnection; + return dbCon == null + ? "NotSupported: " + con.GetType() + : GetReferencedObjects(dbCon); + } + + public static string GetReferencedObjects(DbConnection con) + { + var t = con.GetType(); + + var field = t.GetField("_innerConnection", BindingFlags.Instance | BindingFlags.NonPublic); + if (field == null) throw new Exception("panic: _innerConnection (" + t + ")."); + var innerConnection = field.GetValue(con); + + var tin = innerConnection.GetType(); + + var fi = con is System.Data.SqlClient.SqlConnection + ? tin.BaseType.BaseType.GetField("_referenceCollection", BindingFlags.Instance | BindingFlags.NonPublic) + : tin.BaseType.GetField("_referenceCollection", BindingFlags.Instance | BindingFlags.NonPublic); + if (fi == null) + //return ""; + throw new Exception("panic: referenceCollection."); + + var rc = fi.GetValue(innerConnection); + if (rc == null) + //return ""; + throw new Exception("panic: innerCollection."); + + field = rc.GetType().BaseType.GetField("_items", BindingFlags.Instance | BindingFlags.NonPublic); + if (field == null) throw new Exception("panic: items."); + var items = field.GetValue(rc); + var prop = items.GetType().GetProperty("Length", BindingFlags.Instance | BindingFlags.Public); + if (prop == null) throw new Exception("panic: Length."); + var count = Convert.ToInt32(prop.GetValue(items, null)); + var miGetValue = items.GetType().GetMethod("GetValue", new[] { typeof(int) }); + if (miGetValue == null) throw new Exception("panic: GetValue."); + + if (count == 0) return null; + + StringBuilder result = null; + var hasb = false; + + for (var i = 0; i < count; i++) + { + var referencedObj = miGetValue.Invoke(items, new object[] { i }); + + var hasTargetProp = referencedObj.GetType().GetProperty("HasTarget"); + if (hasTargetProp == null) throw new Exception("panic: HasTarget"); + var hasTarget = Convert.ToBoolean(hasTargetProp.GetValue(referencedObj, null)); + if (hasTarget == false) continue; + + if (hasb == false) + { + result = new StringBuilder(); + result.AppendLine("ReferencedItems"); + hasb = true; + } + + //var inUseProp = referencedObj.GetType().GetProperty("InUse"); + //if (inUseProp == null) throw new Exception("panic: InUse."); + //var inUse = Convert.ToBoolean(inUseProp.GetValue(referencedObj, null)); + var inUse = "?"; + + var targetProp = referencedObj.GetType().GetProperty("Target"); + if (targetProp == null) throw new Exception("panic: Target."); + var objTarget = targetProp.GetValue(referencedObj, null); + + result.AppendFormat("\tDiff.Item id=\"{0}\" inUse=\"{1}\" type=\"{2}\" hashCode=\"{3}\"" + Environment.NewLine, + i, inUse, objTarget.GetType(), objTarget.GetHashCode()); + + DbCommand cmd = null; + if (objTarget is DbDataReader) + { + //var rdr = objTarget as DbDataReader; + try + { + cmd = objTarget.GetType().GetProperty("Command", BindingFlags.Instance | BindingFlags.NonPublic).GetValue(objTarget, null) as DbCommand; + } + catch (Exception e) + { + result.AppendFormat("\t\tObjTarget: DbDataReader, Exception: {0}" + Environment.NewLine, e); + } + } + else if (objTarget is DbCommand) + { + cmd = objTarget as DbCommand; + } + if (cmd == null) + { + result.AppendFormat("\t\tObjTarget: {0}" + Environment.NewLine, objTarget.GetType()); + continue; + } + + result.AppendFormat("\t\tCommand type=\"{0}\" hashCode=\"{1}\"" + Environment.NewLine, + cmd.GetType(), cmd.GetHashCode()); + + var context = GetCommandContext(cmd); + result.AppendFormat("\t\t\tContext: {0}" + Environment.NewLine, context); + + var properties = cmd.GetType().GetProperties(BindingFlags.Instance | BindingFlags.Public); + foreach (var pi in properties) + { + if (pi.PropertyType.IsPrimitive || pi.PropertyType == typeof(string)) + result.AppendFormat("\t\t\t{0}: {1}" + Environment.NewLine, pi.Name, pi.GetValue(cmd, null)); + + if (pi.PropertyType != typeof (DbConnection) || pi.Name != "Connection") continue; + + var con1 = pi.GetValue(cmd, null) as DbConnection; + result.AppendFormat("\t\t\tConnection type=\"{0}\" state=\"{1}\" hashCode=\"{2}\"" + Environment.NewLine, + con1.GetType(), con1.State, con1.GetHashCode()); + + var propertiesCon = con1.GetType().GetProperties(BindingFlags.Instance | BindingFlags.Public); + foreach (var picon in propertiesCon) + { + if (picon.PropertyType.IsPrimitive || picon.PropertyType == typeof(string)) + result.AppendFormat("\t\t\t\t{0}: {1}" + Environment.NewLine, picon.Name, picon.GetValue(con1, null)); + } + } + } + + return result?.ToString(); + } + } +} +#endif diff --git a/src/Umbraco.Core/Persistence/DatabaseScope.cs b/src/Umbraco.Core/Persistence/DatabaseScope.cs new file mode 100644 index 0000000000..e0e3dc1f79 --- /dev/null +++ b/src/Umbraco.Core/Persistence/DatabaseScope.cs @@ -0,0 +1,58 @@ +using System; + +namespace Umbraco.Core.Persistence +{ + public class DatabaseScope : IDisposable + { + private readonly DatabaseScope _parent; + private readonly IDatabaseScopeAccessor _accessor; + private readonly UmbracoDatabaseFactory _factory; + private UmbracoDatabase _database; + private bool _isParent; + private bool _disposed; + private bool _disposeDatabase; + + // can specify a database to create a "substitute" scope eg for deploy - oh my + + internal DatabaseScope(IDatabaseScopeAccessor accessor, UmbracoDatabaseFactory factory, UmbracoDatabase database = null) + { + _accessor = accessor; + _factory = factory; + _database = database; + _parent = _accessor.Scope; + if (_parent != null) _parent._isParent = true; + _accessor.Scope = this; + } + + public UmbracoDatabase Database + { + get + { + if (_disposed) + throw new ObjectDisposedException(null, "Cannot access a disposed object."); + if (_database != null) return _database; + if (_parent != null) return _parent.Database; + _database = _factory.CreateDatabase(); + _disposeDatabase = true; + return _database; + } + } + + public void Dispose() + { + if (_isParent) + throw new InvalidOperationException("Cannot dispose a parent scope."); + if (_disposed) + throw new ObjectDisposedException(null, "Cannot access a disposed object."); + _disposed = true; // fixme race + + if (_disposeDatabase) + _database.Dispose(); + + _accessor.Scope = _parent; + if (_parent != null) _parent._isParent = false; + + GC.SuppressFinalize(this); + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/DbCommandExtensions.cs b/src/Umbraco.Core/Persistence/DbCommandExtensions.cs new file mode 100644 index 0000000000..a13eed3dcf --- /dev/null +++ b/src/Umbraco.Core/Persistence/DbCommandExtensions.cs @@ -0,0 +1,29 @@ +using System.Data; + +namespace Umbraco.Core.Persistence +{ + internal static class DbCommandExtensions + { + /// + /// Unwraps a database command. + /// + /// UmbracoDatabase wraps the original database connection in various layers (see + /// OnConnectionOpened); this unwraps and returns the original database command. + public static IDbCommand UnwrapUmbraco(this IDbCommand command) + { + IDbCommand unwrapped; + + var c = command; + do + { + unwrapped = c; + + var profiled = unwrapped as StackExchange.Profiling.Data.ProfiledDbCommand; + if (profiled != null) unwrapped = profiled.InternalCommand; + + } while (c != unwrapped); + + return unwrapped; + } + } +} diff --git a/src/Umbraco.Core/Persistence/DbConnectionExtensions.cs b/src/Umbraco.Core/Persistence/DbConnectionExtensions.cs index 3fb6c8eaf3..e3d73cf087 100644 --- a/src/Umbraco.Core/Persistence/DbConnectionExtensions.cs +++ b/src/Umbraco.Core/Persistence/DbConnectionExtensions.cs @@ -2,9 +2,9 @@ using System.Data; using System.Data.Common; using System.Linq; -using NPoco; using Umbraco.Core.DI; using Umbraco.Core.Logging; +using Umbraco.Core.Persistence.FaultHandling; namespace Umbraco.Core.Persistence { @@ -68,6 +68,29 @@ namespace Umbraco.Core.Persistence return true; } + /// + /// Unwraps a database connection. + /// + /// UmbracoDatabase wraps the original database connection in various layers (see + /// OnConnectionOpened); this unwraps and returns the original database connection. + internal static IDbConnection UnwrapUmbraco(this IDbConnection connection) + { + IDbConnection unwrapped; + var c = connection; + do + { + unwrapped = c; + + var profiled = unwrapped as StackExchange.Profiling.Data.ProfiledDbConnection; + if (profiled != null) unwrapped = profiled.InnerConnection; + + var retrying = unwrapped as RetryDbConnection; + if (retrying != null) unwrapped = retrying.Inner; + + } while (c != unwrapped); + + return unwrapped; + } } } diff --git a/src/Umbraco.Core/Persistence/Factories/MemberTypeReadOnlyFactory.cs b/src/Umbraco.Core/Persistence/Factories/MemberTypeReadOnlyFactory.cs index b4d5d2e566..ba7a47165b 100644 --- a/src/Umbraco.Core/Persistence/Factories/MemberTypeReadOnlyFactory.cs +++ b/src/Umbraco.Core/Persistence/Factories/MemberTypeReadOnlyFactory.cs @@ -9,9 +9,10 @@ namespace Umbraco.Core.Persistence.Factories { internal class MemberTypeReadOnlyFactory { - public IMemberType BuildEntity(MemberTypeReadOnlyDto dto) + public IMemberType BuildEntity(MemberTypeReadOnlyDto dto, out bool needsSaving) { var standardPropertyTypes = Constants.Conventions.Member.GetStandardPropertyTypeStubs(); + needsSaving = false; var memberType = new MemberType(dto.ParentId); @@ -47,6 +48,12 @@ namespace Umbraco.Core.Persistence.Factories { if (dto.PropertyTypes.Any(x => x.Alias.Equals(standardPropertyType.Key))) continue; + // beware! + // means that we can return a memberType "from database" that has some property types + // that do *not* come from the database and therefore are incomplete eg have no key, + // no id, no dataTypeDefinitionId - ouch! - better notify caller of the situation + needsSaving = true; + //Add the standard PropertyType to the current list propertyTypes.Add(standardPropertyType.Value); diff --git a/src/Umbraco.Core/Persistence/IDatabaseScopeAccessor.cs b/src/Umbraco.Core/Persistence/IDatabaseScopeAccessor.cs new file mode 100644 index 0000000000..2799114cc5 --- /dev/null +++ b/src/Umbraco.Core/Persistence/IDatabaseScopeAccessor.cs @@ -0,0 +1,10 @@ +namespace Umbraco.Core.Persistence +{ + /// + /// Provides access to DatabaseScope. + /// + public interface IDatabaseScopeAccessor + { + DatabaseScope Scope { get; set; } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/Repositories/MemberTypeRepository.cs b/src/Umbraco.Core/Persistence/Repositories/MemberTypeRepository.cs index 4a49af7ded..fc38520936 100644 --- a/src/Umbraco.Core/Persistence/Repositories/MemberTypeRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/MemberTypeRepository.cs @@ -316,13 +316,19 @@ namespace Umbraco.Core.Persistence.Repositories /// /// /// - private static IEnumerable BuildFromDtos(List dtos) + private IEnumerable BuildFromDtos(List dtos) { if (dtos == null || dtos.Any() == false) return Enumerable.Empty(); var factory = new MemberTypeReadOnlyFactory(); - return dtos.Select(factory.BuildEntity); + return dtos.Select(x => + { + bool needsSaving; + var memberType = factory.BuildEntity(x, out needsSaving); + if (needsSaving) PersistUpdatedItem(memberType); + return memberType; + }).ToList(); } /// diff --git a/src/Umbraco.Core/Persistence/UmbracoDatabase.cs b/src/Umbraco.Core/Persistence/UmbracoDatabase.cs index e915b32138..4a5b77e4bf 100644 --- a/src/Umbraco.Core/Persistence/UmbracoDatabase.cs +++ b/src/Umbraco.Core/Persistence/UmbracoDatabase.cs @@ -16,7 +16,7 @@ namespace Umbraco.Core.Persistence /// /// Is used everywhere in place of the original NPoco Database object, and provides additional features /// such as profiling, retry policies, logging, etc. - /// Is never created directly but obtained from the . + /// Is never created directly but obtained from the . /// It implements IDisposeOnRequestEnd which means it will be disposed when the request ends, which /// automatically closes the connection - as implemented by NPoco Database.Dispose(). /// @@ -29,48 +29,17 @@ namespace Umbraco.Core.Persistence private readonly SqlContext _sqlContext; private readonly RetryPolicy _connectionRetryPolicy; private readonly RetryPolicy _commandRetryPolicy; - private bool _enableCount; + + #region Ctor /// - /// Used for testing + /// Initializes a new instance of the class. /// - internal Guid InstanceId { get; } = Guid.NewGuid(); - - public ISqlSyntaxProvider SqlSyntax => _sqlContext.SqlSyntax; - - /// - /// Generally used for testing, will output all SQL statements executed to the logger - /// - internal bool EnableSqlTrace { get; set; } - - /// - /// Used for testing - /// - internal void EnableSqlCount() - { - _enableCount = true; - } - - /// - /// Used for testing - /// - internal void DisableSqlCount() - { - _enableCount = false; - SqlCount = 0; - } - - /// - /// Used for testing - /// - internal int SqlCount { get; private set; } - - // used by DefaultDatabaseFactory - // creates one instance per request - // also used by DatabaseContext for creating DBs and upgrading - public UmbracoDatabase(string connectionString, - SqlContext sqlContext, DbProviderFactory provider, ILogger logger, - RetryPolicy connectionRetryPolicy = null, RetryPolicy commandRetryPolicy = null) + /// + /// Used by UmbracoDatabaseFactory to create databases. + /// Also used by DatabaseBuilder for creating databases and installing/upgrading. + /// + public UmbracoDatabase(string connectionString, SqlContext sqlContext, DbProviderFactory provider, ILogger logger, RetryPolicy connectionRetryPolicy = null, RetryPolicy commandRetryPolicy = null) : base(connectionString, sqlContext.DatabaseType, provider, DefaultIsolationLevel) { _sqlContext = sqlContext; @@ -79,41 +48,134 @@ namespace Umbraco.Core.Persistence _connectionRetryPolicy = connectionRetryPolicy; _commandRetryPolicy = commandRetryPolicy; - EnableSqlTrace = false; + EnableSqlTrace = EnableSqlTraceDefault; } - // INTERNAL FOR UNIT TESTS - internal UmbracoDatabase(DbConnection connection, - SqlContext sqlContext, ILogger logger) + /// + /// Initializes a new instance of the class. + /// + /// Internal for unit tests only. + internal UmbracoDatabase(DbConnection connection, SqlContext sqlContext, ILogger logger) : base(connection, sqlContext.DatabaseType, DefaultIsolationLevel) { _sqlContext = sqlContext; - _logger = logger; - EnableSqlTrace = false; + EnableSqlTrace = EnableSqlTraceDefault; } - // fixme: these two could be an extension method of IUmbracoDatabaseConfig + #endregion + + /// + /// Gets the database Sql syntax. + /// + public ISqlSyntaxProvider SqlSyntax => _sqlContext.SqlSyntax; + + /// + /// Creates a Sql statement. + /// public Sql Sql() { return NPoco.Sql.BuilderFor(_sqlContext); } + /// + /// Creates a Sql statement. + /// public Sql Sql(string sql, params object[] args) { return Sql().Append(sql, args); } - //protected override void OnConnectionClosing(DbConnection conn) - //{ - // base.OnConnectionClosing(conn); - //} + #region Testing, Debugging and Troubleshooting + + private bool _enableCount; + +#if DEBUG_DATABASES + private int _spid = -1; + private const bool EnableSqlTraceDefault = true; +#else + private string _sid; + private const bool EnableSqlTraceDefault = false; +#endif + + /// + /// Gets this instance's unique identifier. + /// + public Guid InstanceId { get; } = Guid.NewGuid(); + + /// + /// Gets this instance's string identifier. + /// + public string InstanceSid { + get + { +#if DEBUG_DATABASES + return InstanceId.ToString("N").Substring(0, 8) + ':' + _spid; +#else + return _sid ?? (_sid = InstanceId.ToString("N").Substring(0, 8)); +#endif + } + } + + /// + /// Gets or sets a value indicating whether to log all executed Sql statements. + /// + internal bool EnableSqlTrace { get; set; } + + /// + /// Gets or sets a value indicating whether to count all executed Sql statements. + /// + internal bool EnableSqlCount + { + get { return _enableCount; } + set + { + _enableCount = value; + if (_enableCount == false) + SqlCount = 0; + } + } + + /// + /// Gets the count of all executed Sql statements. + /// + internal int SqlCount { get; private set; } + + #endregion + + #region OnSomething + + // fixme.poco - has new interceptors to replace OnSomething? protected override DbConnection OnConnectionOpened(DbConnection connection) { if (connection == null) throw new ArgumentNullException(nameof(connection)); +#if DEBUG_DATABASES + if (DatabaseType == DBType.MySql) + { + using (var command = connection.CreateCommand()) + { + command.CommandText = "SELECT CONNECTION_ID()"; + _spid = Convert.ToInt32(command.ExecuteScalar()); + } + } + else if (DatabaseType == DBType.SqlServer) + { + using (var command = connection.CreateCommand()) + { + command.CommandText = "SELECT @@SPID"; + _spid = Convert.ToInt32(command.ExecuteScalar()); + } + } + else + { + // includes SqlCE + _spid = 0; + } +#endif + // wrap the connection with a profiling connection that tracks timings connection = new StackExchange.Profiling.Data.ProfiledDbConnection(connection, MiniProfiler.Current); @@ -124,14 +186,20 @@ namespace Umbraco.Core.Persistence return connection; } +#if DEBUG_DATABASES + public override void OnConnectionClosing(IDbConnection conn) + { + _spid = -1; + base.OnConnectionClosing(conn); + } +#endif + protected override void OnException(Exception x) { - _logger.Error("Database exception occurred", x); + _logger.Error("Exception (" + InstanceSid + ").", x); base.OnException(x); } - // fixme.poco - has new interceptors? - protected override void OnExecutingCommand(DbCommand cmd) { // if no timeout is specified, and the connection has a longer timeout, use it @@ -141,6 +209,10 @@ namespace Umbraco.Core.Persistence if (EnableSqlTrace) { var sb = new StringBuilder(); +#if DEBUG_DATABASES + sb.Append(InstanceSid); + sb.Append(": "); +#endif sb.Append(cmd.CommandText); foreach (DbParameter p in cmd.Parameters) { @@ -148,21 +220,38 @@ namespace Umbraco.Core.Persistence sb.Append(p.Value); } - _logger.Debug(sb.ToString()); + _logger.Debug(sb.ToString().Replace("{", "{{").Replace("}", "}}")); } +#if DEBUG_DATABASES + // detects whether the command is already in use (eg still has an open reader...) + DatabaseDebugHelper.SetCommand(cmd, InstanceSid + " [T" + Thread.CurrentThread.ManagedThreadId + "]"); + var refsobj = DatabaseDebugHelper.GetReferencedObjects(cmd.Connection); + if (refsobj != null) _logger.Debug("Oops!" + Environment.NewLine + refsobj); +#endif + base.OnExecutingCommand(cmd); } protected override void OnExecutedCommand(DbCommand cmd) { if (_enableCount) - { SqlCount++; - } + base.OnExecutedCommand(cmd); } - // fixme - see v7.6 - what about disposing & managing context and call context? + #endregion + + // at the moment, NPoco does not support overriding Dispose + /* + public override void Dispose(bool disposing) + { +#if DEBUG_DATABASES + LogHelper.Debug("Dispose (" + InstanceSid + ")."); +#endif + base.Dispose(); + } + */ } } \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/DefaultDatabaseFactory.cs b/src/Umbraco.Core/Persistence/UmbracoDatabaseFactory.cs similarity index 60% rename from src/Umbraco.Core/Persistence/DefaultDatabaseFactory.cs rename to src/Umbraco.Core/Persistence/UmbracoDatabaseFactory.cs index 011ecbd971..fd417505b3 100644 --- a/src/Umbraco.Core/Persistence/DefaultDatabaseFactory.cs +++ b/src/Umbraco.Core/Persistence/UmbracoDatabaseFactory.cs @@ -1,292 +1,380 @@ -using System; -using System.Collections.Generic; -using System.Configuration; -using System.Data.Common; -using System.Linq; -using System.Threading; -using NPoco; -using NPoco.FluentMappings; -using Umbraco.Core.Configuration; -using Umbraco.Core.Exceptions; -using Umbraco.Core.Logging; -using Umbraco.Core.Persistence.FaultHandling; -using Umbraco.Core.Persistence.Mappers; -using Umbraco.Core.Persistence.Querying; -using Umbraco.Core.Persistence.SqlSyntax; - -namespace Umbraco.Core.Persistence -{ - /// - /// Default implementation of . - /// - /// - /// This factory implementation creates and manages an "ambient" database connection. When running - /// within an Http context, "ambient" means "associated with that context". Otherwise, it means "static to - /// the current thread". In this latter case, note that the database connection object is not thread safe. - /// It wraps an NPoco DatabaseFactory which is initializes with a proper IPocoDataFactory to ensure - /// that NPoco's plumbing is cached appropriately for the whole application. - /// - internal class DefaultDatabaseFactory : DisposableObject, IDatabaseFactory - { - private readonly IUmbracoDatabaseAccessor _umbracoDatabaseAccessor; - private readonly ISqlSyntaxProvider[] _sqlSyntaxProviders; - private readonly IMapperCollection _mappers; - private readonly ILogger _logger; - - private DatabaseFactory _databaseFactory; - private IPocoDataFactory _pocoDataFactory; - private string _connectionString; - private string _providerName; - private DbProviderFactory _dbProviderFactory; - private DatabaseType _databaseType; - private ISqlSyntaxProvider _sqlSyntax; - private IQueryFactory _queryFactory; - private SqlContext _sqlContext; - private RetryPolicy _connectionRetryPolicy; - private RetryPolicy _commandRetryPolicy; - private readonly ReaderWriterLockSlim _lock = new ReaderWriterLockSlim(); - - /// - /// Initializes a new instance of the with the default connection, and a logger. - /// - /// The collection of available sql syntax providers. - /// A logger. - /// - /// - /// Used by LightInject. - public DefaultDatabaseFactory(IEnumerable sqlSyntaxProviders, ILogger logger, IUmbracoDatabaseAccessor umbracoDatabaseAccessor, IMapperCollection mappers) - : this(GlobalSettings.UmbracoConnectionName, sqlSyntaxProviders, logger, umbracoDatabaseAccessor, mappers) - { - if (Configured == false) - DatabaseBuilder.GiveLegacyAChance(this, logger); - } - - /// - /// Initializes a new instance of the with a connection string name and a logger. - /// - /// The name of the connection string in web.config. - /// The collection of available sql syntax providers. - /// A logger - /// - /// - /// Used by the other ctor and in tests. - public DefaultDatabaseFactory(string connectionStringName, IEnumerable sqlSyntaxProviders, ILogger logger, IUmbracoDatabaseAccessor umbracoDatabaseAccessor, IMapperCollection mappers) - { - if (sqlSyntaxProviders == null) throw new ArgumentNullException(nameof(sqlSyntaxProviders)); - if (logger == null) throw new ArgumentNullException(nameof(logger)); - if (umbracoDatabaseAccessor == null) throw new ArgumentNullException(nameof(umbracoDatabaseAccessor)); - if (string.IsNullOrWhiteSpace(connectionStringName)) throw new ArgumentNullOrEmptyException(nameof(connectionStringName)); - if (mappers == null) throw new ArgumentNullException(nameof(mappers)); - - _mappers = mappers; - _sqlSyntaxProviders = sqlSyntaxProviders.ToArray(); - _logger = logger; - _umbracoDatabaseAccessor = umbracoDatabaseAccessor; - - var settings = ConfigurationManager.ConnectionStrings[connectionStringName]; - if (settings == null) - return; // not configured - - // could as well be - // so need to test the values too - var connectionString = settings.ConnectionString; - var providerName = settings.ProviderName; - if (string.IsNullOrWhiteSpace(connectionString) || string.IsNullOrWhiteSpace(providerName)) - { - logger.Debug("Missing connection string or provider name, defer configuration."); - return; // not configured - } - - Configure(settings.ConnectionString, settings.ProviderName); - } - - /// - /// Initializes a new instance of the with a connection string, a provider name and a logger. - /// - /// The database connection string. - /// The name of the database provider. - /// The collection of available sql syntax providers. - /// A logger. - /// - /// - /// Used in tests. - public DefaultDatabaseFactory(string connectionString, string providerName, IEnumerable sqlSyntaxProviders, ILogger logger, IUmbracoDatabaseAccessor umbracoDatabaseAccessor, IMapperCollection mappers) - { - if (sqlSyntaxProviders == null) throw new ArgumentNullException(nameof(sqlSyntaxProviders)); - if (logger == null) throw new ArgumentNullException(nameof(logger)); - if (umbracoDatabaseAccessor == null) throw new ArgumentNullException(nameof(umbracoDatabaseAccessor)); - if (mappers == null) throw new ArgumentNullException(nameof(mappers)); - - _mappers = mappers; - _sqlSyntaxProviders = sqlSyntaxProviders.ToArray(); - _logger = logger; - _umbracoDatabaseAccessor = umbracoDatabaseAccessor; - - if (string.IsNullOrWhiteSpace(connectionString) || string.IsNullOrWhiteSpace(providerName)) - { - logger.Debug("Missing connection string or provider name, defer configuration."); - return; // not configured - } - - Configure(connectionString, providerName); - } - - /// - /// Gets a value indicating whether the database is configured (no connect test). - /// - /// - public bool Configured { get; private set; } - - /// - /// Gets a value indicating whether it is possible to connect to the database. - /// - public bool CanConnect => Configured && DbConnectionExtensions.IsConnectionAvailable(_connectionString, _providerName); - - /// - /// Gets the database sql syntax provider. - /// - public ISqlSyntaxProvider SqlSyntax - { - get - { - EnsureConfigured(); - return _sqlSyntax; - } - } - - /// - /// Gets the database query factory. - /// - public IQueryFactory QueryFactory { - get - { - EnsureConfigured(); - return _queryFactory; - } - } - - public Sql Sql() => NPoco.Sql.BuilderFor(_sqlContext); - - // will be configured by the database context - public void Configure(string connectionString, string providerName) - { - using (new WriteLock(_lock)) - { - _logger.Debug("Configuring."); - - if (Configured) throw new InvalidOperationException("Already configured."); - - if (connectionString.IsNullOrWhiteSpace()) throw new ArgumentNullException(nameof(connectionString)); - if (providerName.IsNullOrWhiteSpace()) throw new ArgumentNullException(nameof(providerName)); - - _connectionString = connectionString; - _providerName = providerName; - - _connectionRetryPolicy = RetryPolicyFactory.GetDefaultSqlConnectionRetryPolicyByConnectionString(_connectionString); - _commandRetryPolicy = RetryPolicyFactory.GetDefaultSqlCommandRetryPolicyByConnectionString(_connectionString); - - _dbProviderFactory = DbProviderFactories.GetFactory(_providerName); - if (_dbProviderFactory == null) - throw new Exception($"Can't find a provider factory for provider name \"{_providerName}\"."); - _databaseType = DatabaseType.Resolve(_dbProviderFactory.GetType().Name, _providerName); - if (_databaseType == null) - throw new Exception($"Can't find an NPoco database type for provider name \"{_providerName}\"."); - - _sqlSyntax = GetSqlSyntaxProvider(_providerName); - if (_sqlSyntax == null) - throw new Exception($"Can't find a sql syntax provider for provider name \"{_providerName}\"."); - - // ensure we have only 1 set of mappers, and 1 PocoDataFactory, for all database - // so that everything NPoco is properly cached for the lifetime of the application - var mappers = new NPoco.MapperCollection { new PocoMapper() }; - var factory = new FluentPocoDataFactory((type, iPocoDataFactory) => new PocoDataBuilder(type, mappers).Init()); - _pocoDataFactory = factory; - var config = new FluentConfig(xmappers => factory); - - // create the database factory - _databaseFactory = DatabaseFactory.Config(x => x - .UsingDatabase(CreateDatabaseInstance) // creating UmbracoDatabase instances - .WithFluentConfig(config)); // with proper configuration - - if (_databaseFactory == null) throw new NullReferenceException("The call to DatabaseFactory.Config yielded a null DatabaseFactory instance."); - - // these are created here because it is the DefaultDatabaseFactory that determines - // the sql syntax, poco data factory, and database type - so it "owns" the context - // and the query factory - _sqlContext = new SqlContext(_sqlSyntax, _pocoDataFactory, _databaseType); - _queryFactory = new QueryFactory(_sqlSyntax, _mappers); - - _logger.Debug("Configured."); - Configured = true; - } - } - - // gets the sql syntax provider that corresponds, from attribute - private ISqlSyntaxProvider GetSqlSyntaxProvider(string providerName) - { - var name = providerName.ToLowerInvariant(); - var provider = _sqlSyntaxProviders.FirstOrDefault(x => - x.GetType() - .FirstAttribute() - .ProviderName.ToLowerInvariant() - .Equals(name)); - if (provider != null) return provider; - throw new InvalidOperationException($"Unknown provider name \"{providerName}\""); - - // previously we'd try to return SqlServerSyntaxProvider by default but this is bad - //provider = _syntaxProviders.FirstOrDefault(x => x.GetType() == typeof(SqlServerSyntaxProvider)); - } - - private void EnsureConfigured() - { - using (new ReadLock(_lock)) - { - if (Configured == false) - throw new InvalidOperationException("Not configured."); - } - } - - // method used by NPoco's DatabaseFactory to actually create the database instance - private UmbracoDatabase CreateDatabaseInstance() - { - return new UmbracoDatabase(_connectionString, _sqlContext, _dbProviderFactory, _logger, _connectionRetryPolicy, _commandRetryPolicy); - } - - /// - /// Gets (creates or retrieves) the "ambient" database connection. - /// - /// The "ambient" database connection. - public UmbracoDatabase GetDatabase() - { - EnsureConfigured(); - - // check if it's in scope - var db = _umbracoDatabaseAccessor.UmbracoDatabase; - if (db != null) return db; - db = (UmbracoDatabase) _databaseFactory.GetDatabase(); - _umbracoDatabaseAccessor.UmbracoDatabase = db; - return db; - } - - protected override void DisposeResources() - { - // this is weird, because hybrid accessors store different databases per - // thread, so we don't really know what we are disposing here... - // besides, we don't really want to dispose the factory, which is a singleton... - - var db = _umbracoDatabaseAccessor.UmbracoDatabase; - _umbracoDatabaseAccessor.UmbracoDatabase = null; - db?.Dispose(); - Configured = false; - } - - // during tests, the thread static var can leak between tests - // this method provides a way to force-reset the variable - internal void ResetForTests() - { - var db = _umbracoDatabaseAccessor.UmbracoDatabase; - _umbracoDatabaseAccessor.UmbracoDatabase = null; - db?.Dispose(); - } - } +using System; +using System.Collections.Generic; +using System.Configuration; +using System.Data.Common; +using System.Linq; +using System.Threading; +using NPoco; +using NPoco.FluentMappings; +using Umbraco.Core.Configuration; +using Umbraco.Core.Exceptions; +using Umbraco.Core.Logging; +using Umbraco.Core.Persistence.FaultHandling; +using Umbraco.Core.Persistence.Mappers; +using Umbraco.Core.Persistence.Querying; +using Umbraco.Core.Persistence.SqlSyntax; + +namespace Umbraco.Core.Persistence +{ + /// + /// Default implementation of . + /// + /// + /// This factory implementation creates and manages an "ambient" database connection. When running + /// within an Http context, "ambient" means "associated with that context". Otherwise, it means "static to + /// the current thread". In this latter case, note that the database connection object is not thread safe. + /// It wraps an NPoco DatabaseFactory which is initializes with a proper IPocoDataFactory to ensure + /// that NPoco's plumbing is cached appropriately for the whole application. + /// + internal class UmbracoDatabaseFactory : DisposableObject, IDatabaseFactory + { + //private readonly IUmbracoDatabaseAccessor _umbracoDatabaseAccessor; + private readonly IDatabaseScopeAccessor _databaseScopeAccessor; + private readonly ISqlSyntaxProvider[] _sqlSyntaxProviders; + private readonly IMapperCollection _mappers; + private readonly ILogger _logger; + + private DatabaseFactory _npocoDatabaseFactory; + private IPocoDataFactory _pocoDataFactory; + private string _connectionString; + private string _providerName; + private DbProviderFactory _dbProviderFactory; + private DatabaseType _databaseType; + private ISqlSyntaxProvider _sqlSyntax; + private IQueryFactory _queryFactory; + private SqlContext _sqlContext; + private RetryPolicy _connectionRetryPolicy; + private RetryPolicy _commandRetryPolicy; + private readonly ReaderWriterLockSlim _lock = new ReaderWriterLockSlim(); + + #region Ctor + + /// + /// Initializes a new instance of the . + /// + /// Used by LightInject. + public UmbracoDatabaseFactory(IEnumerable sqlSyntaxProviders, ILogger logger, IDatabaseScopeAccessor databaseScopeAccessor, IMapperCollection mappers) + : this(GlobalSettings.UmbracoConnectionName, sqlSyntaxProviders, logger, databaseScopeAccessor, mappers) + { + if (Configured == false) + DatabaseBuilder.GiveLegacyAChance(this, logger); + } + + /// + /// Initializes a new instance of the . + /// + /// Used by the other ctor and in tests. + public UmbracoDatabaseFactory(string connectionStringName, IEnumerable sqlSyntaxProviders, ILogger logger, IDatabaseScopeAccessor databaseScopeAccessor, IMapperCollection mappers) + { + if (sqlSyntaxProviders == null) throw new ArgumentNullException(nameof(sqlSyntaxProviders)); + if (logger == null) throw new ArgumentNullException(nameof(logger)); + if (databaseScopeAccessor == null) throw new ArgumentNullException(nameof(databaseScopeAccessor)); + if (string.IsNullOrWhiteSpace(connectionStringName)) throw new ArgumentNullOrEmptyException(nameof(connectionStringName)); + if (mappers == null) throw new ArgumentNullException(nameof(mappers)); + + _mappers = mappers; + _sqlSyntaxProviders = sqlSyntaxProviders.ToArray(); + _logger = logger; + _databaseScopeAccessor = databaseScopeAccessor; + + var settings = ConfigurationManager.ConnectionStrings[connectionStringName]; + if (settings == null) + return; // not configured + + // could as well be + // so need to test the values too + var connectionString = settings.ConnectionString; + var providerName = settings.ProviderName; + if (string.IsNullOrWhiteSpace(connectionString) || string.IsNullOrWhiteSpace(providerName)) + { + logger.Debug("Missing connection string or provider name, defer configuration."); + return; // not configured + } + + Configure(settings.ConnectionString, settings.ProviderName); + } + + /// + /// Initializes a new instance of the . + /// + /// Used in tests. + public UmbracoDatabaseFactory(string connectionString, string providerName, IEnumerable sqlSyntaxProviders, ILogger logger, IDatabaseScopeAccessor databaseScopeAccessor, IMapperCollection mappers) + { + if (sqlSyntaxProviders == null) throw new ArgumentNullException(nameof(sqlSyntaxProviders)); + if (logger == null) throw new ArgumentNullException(nameof(logger)); + if (databaseScopeAccessor == null) throw new ArgumentNullException(nameof(databaseScopeAccessor)); + if (mappers == null) throw new ArgumentNullException(nameof(mappers)); + + _mappers = mappers; + _sqlSyntaxProviders = sqlSyntaxProviders.ToArray(); + _logger = logger; + _databaseScopeAccessor = databaseScopeAccessor; + + if (string.IsNullOrWhiteSpace(connectionString) || string.IsNullOrWhiteSpace(providerName)) + { + logger.Debug("Missing connection string or provider name, defer configuration."); + return; // not configured + } + + Configure(connectionString, providerName); + } + + #endregion + + /// + /// Gets a value indicating whether the database is configured (no connect test). + /// + /// + public bool Configured { get; private set; } + + /// + /// Gets a value indicating whether it is possible to connect to the database. + /// + public bool CanConnect => Configured && DbConnectionExtensions.IsConnectionAvailable(_connectionString, _providerName); + + /// + /// Gets the database sql syntax provider. + /// + public ISqlSyntaxProvider SqlSyntax + { + get + { + EnsureConfigured(); + return _sqlSyntax; + } + } + + /// + /// Gets the database query factory. + /// + public IQueryFactory QueryFactory { + get + { + EnsureConfigured(); + return _queryFactory; + } + } + + public Sql Sql() => NPoco.Sql.BuilderFor(_sqlContext); + + // will be configured by the database context + public void Configure(string connectionString, string providerName) + { + using (new WriteLock(_lock)) + { + _logger.Debug("Configuring."); + + if (Configured) throw new InvalidOperationException("Already configured."); + + if (connectionString.IsNullOrWhiteSpace()) throw new ArgumentNullException(nameof(connectionString)); + if (providerName.IsNullOrWhiteSpace()) throw new ArgumentNullException(nameof(providerName)); + + _connectionString = connectionString; + _providerName = providerName; + + _connectionRetryPolicy = RetryPolicyFactory.GetDefaultSqlConnectionRetryPolicyByConnectionString(_connectionString); + _commandRetryPolicy = RetryPolicyFactory.GetDefaultSqlCommandRetryPolicyByConnectionString(_connectionString); + + _dbProviderFactory = DbProviderFactories.GetFactory(_providerName); + if (_dbProviderFactory == null) + throw new Exception($"Can't find a provider factory for provider name \"{_providerName}\"."); + _databaseType = DatabaseType.Resolve(_dbProviderFactory.GetType().Name, _providerName); + if (_databaseType == null) + throw new Exception($"Can't find an NPoco database type for provider name \"{_providerName}\"."); + + _sqlSyntax = GetSqlSyntaxProvider(_providerName); + if (_sqlSyntax == null) + throw new Exception($"Can't find a sql syntax provider for provider name \"{_providerName}\"."); + + // ensure we have only 1 set of mappers, and 1 PocoDataFactory, for all database + // so that everything NPoco is properly cached for the lifetime of the application + var mappers = new NPoco.MapperCollection { new PocoMapper() }; + var factory = new FluentPocoDataFactory((type, iPocoDataFactory) => new PocoDataBuilder(type, mappers).Init()); + _pocoDataFactory = factory; + var config = new FluentConfig(xmappers => factory); + + // create the database factory + _npocoDatabaseFactory = DatabaseFactory.Config(x => x + .UsingDatabase(CreateDatabaseInstance) // creating UmbracoDatabase instances + .WithFluentConfig(config)); // with proper configuration + + if (_npocoDatabaseFactory == null) throw new NullReferenceException("The call to DatabaseFactory.Config yielded a null DatabaseFactory instance."); + + // these are created here because it is the UmbracoDatabaseFactory that determines + // the sql syntax, poco data factory, and database type - so it "owns" the context + // and the query factory + _sqlContext = new SqlContext(_sqlSyntax, _pocoDataFactory, _databaseType); + _queryFactory = new QueryFactory(_sqlSyntax, _mappers); + + _logger.Debug("Configured."); + Configured = true; + } + } + + // gets the sql syntax provider that corresponds, from attribute + private ISqlSyntaxProvider GetSqlSyntaxProvider(string providerName) + { + var name = providerName.ToLowerInvariant(); + var provider = _sqlSyntaxProviders.FirstOrDefault(x => + x.GetType() + .FirstAttribute() + .ProviderName.ToLowerInvariant() + .Equals(name)); + if (provider != null) return provider; + throw new InvalidOperationException($"Unknown provider name \"{providerName}\""); + + // previously we'd try to return SqlServerSyntaxProvider by default but this is bad + //provider = _syntaxProviders.FirstOrDefault(x => x.GetType() == typeof(SqlServerSyntaxProvider)); + } + + private void EnsureConfigured() + { + using (new ReadLock(_lock)) + { + if (Configured == false) + throw new InvalidOperationException("Not configured."); + } + } + + // method used by NPoco's DatabaseFactory to actually create the database instance + private UmbracoDatabase CreateDatabaseInstance() + { + return new UmbracoDatabase(_connectionString, _sqlContext, _dbProviderFactory, _logger, _connectionRetryPolicy, _commandRetryPolicy); + } + + // fixme temp? + public UmbracoDatabase Database => GetDatabase(); + + /// + /// Gets (creates or retrieves) the ambient database connection. + /// + /// The ambient database connection. + public UmbracoDatabase GetDatabase() + { + EnsureConfigured(); + + var scope = _databaseScopeAccessor.Scope; + if (scope == null) throw new InvalidOperationException("Out of scope."); + return scope.Database; + + //// check if it's in scope + //var db = _umbracoDatabaseAccessor.UmbracoDatabase; + //if (db != null) return db; + //db = (UmbracoDatabase) _npocoDatabaseFactory.GetDatabase(); + //_umbracoDatabaseAccessor.UmbracoDatabase = db; + //return db; + } + + /// + /// Creates a new database instance. + /// + /// The database instance is not part of any scope and must be disposed after being used. + public UmbracoDatabase CreateDatabase() + { + return (UmbracoDatabase) _npocoDatabaseFactory.GetDatabase(); + } + + protected override void DisposeResources() + { + // this is weird, because hybrid accessors store different databases per + // thread, so we don't really know what we are disposing here... + // besides, we don't really want to dispose the factory, which is a singleton... + + // fixme - does not make any sense! + //var db = _umbracoDatabaseAccessor.UmbracoDatabase; + //_umbracoDatabaseAccessor.UmbracoDatabase = null; + //db?.Dispose(); + Configured = false; + } + + // during tests, the thread static var can leak between tests + // this method provides a way to force-reset the variable + internal void ResetForTests() + { + // fixme - does not make any sense! + //var db = _umbracoDatabaseAccessor.UmbracoDatabase; + //_umbracoDatabaseAccessor.UmbracoDatabase = null; + //db?.Dispose(); + _databaseScopeAccessor.Scope = null; + } + + //public bool HasAmbient => _umbracoDatabaseAccessor.UmbracoDatabase != null; + + //public UmbracoDatabase DetachAmbient() + //{ + // var database = _umbracoDatabaseAccessor.UmbracoDatabase; + // _umbracoDatabaseAccessor.UmbracoDatabase = null; + // return database; + //} + + //public void AttachAmbient(UmbracoDatabase database) + //{ + // var tmp = _umbracoDatabaseAccessor.UmbracoDatabase; + // _umbracoDatabaseAccessor.UmbracoDatabase = database; + // tmp?.Dispose(); + + // // fixme - what shall we do with tmp? + // // fixme - what about using "disposing" of the database to remove it from "ambient"?! + //} + + //public IDisposable CreateScope(bool force = false) // fixme - why would we ever force? + //{ + // if (HasAmbient) + // { + // return force + // ? new DatabaseScope(this, DetachAmbient(), GetDatabase()) + // : new DatabaseScope(this, null, null); + // } + + // // create a new, temp, database (will be disposed with DatabaseScope) + // return new DatabaseScope(this, null, GetDatabase()); + //} + + public IDisposable CreateScope() + { + return new DatabaseScope(_databaseScopeAccessor, this); + } + + /* + private class DatabaseScope : IDisposable + { + private readonly UmbracoDatabaseFactory _factory; + private readonly UmbracoDatabase _orig; + private readonly UmbracoDatabase _temp; + + // orig is the original database that was ambient when the scope was created + // if not null, it has been detached in order to be replaced by temp, which cannot be null + // if null, either there was no ambient database, or we don't want to replace it + // temp is the scope database that is created for the scope + // if not null, it has been attached and is not the ambient database, + // and when the scope is disposed it will be detached, disposed, and replaced by orig + // if null, the scope is nested and reusing the ambient database, without touching anything + + public DatabaseScope(UmbracoDatabaseFactory factory, UmbracoDatabase orig, UmbracoDatabase temp) + { + if (factory == null) throw new ArgumentNullException(nameof(factory)); + _factory = factory; + + _orig = orig; + _temp = temp; + } + + public void Dispose() + { + if (_temp != null) // if the scope had its own database + { + // detach and ensure consistency, then dispose + var temp = _factory.DetachAmbient(); + if (temp != _temp) throw new Exception("bam!"); + temp.Dispose(); + + // re-instate original database if any + if (_orig != null) + _factory.AttachAmbient(_orig); + } + GC.SuppressFinalize(this); + } + } + */ + } } \ No newline at end of file diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index 8746a0a1db..120f8586ff 100644 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -295,7 +295,9 @@ + + @@ -305,6 +307,7 @@ + @@ -389,6 +392,7 @@ + @@ -823,7 +827,7 @@ - + diff --git a/src/Umbraco.Core/UmbracoApplicationBase.cs b/src/Umbraco.Core/UmbracoApplicationBase.cs index 0be00acfe2..84ea1a7151 100644 --- a/src/Umbraco.Core/UmbracoApplicationBase.cs +++ b/src/Umbraco.Core/UmbracoApplicationBase.cs @@ -13,7 +13,6 @@ namespace Umbraco.Core /// /// Provides an abstract base class for the Umbraco HttpApplication. /// - /// This is exposed in the core so that we can have the IApplicationEventHandler in the core project so that public abstract class UmbracoApplicationBase : HttpApplication { private IRuntime _runtime; diff --git a/src/Umbraco.Tests.Benchmarks/BulkInsertBenchmarks.cs b/src/Umbraco.Tests.Benchmarks/BulkInsertBenchmarks.cs index d30b5929ed..f987be5f32 100644 --- a/src/Umbraco.Tests.Benchmarks/BulkInsertBenchmarks.cs +++ b/src/Umbraco.Tests.Benchmarks/BulkInsertBenchmarks.cs @@ -49,7 +49,7 @@ namespace Umbraco.Tests.Benchmarks IDatabaseFactory f = null; var l = new Lazy(() => f); var p = new SqlServerSyntaxProvider(l); - f = new DefaultDatabaseFactory( + f = new UmbracoDatabaseFactory( "server=.\\SQLExpress;database=YOURDB;user id=YOURUSER;password=YOURPASS", Constants.DatabaseProviders.SqlServer, new [] { p }, @@ -61,7 +61,7 @@ namespace Umbraco.Tests.Benchmarks private UmbracoDatabase GetSqlCeDatabase(string cstr, ILogger logger) { - var f = new DefaultDatabaseFactory( + var f = new UmbracoDatabaseFactory( cstr, Constants.DatabaseProviders.SqlCe, new[] { new SqlCeSyntaxProvider() }, diff --git a/src/Umbraco.Tests/Persistence/DatabaseContextTests.cs b/src/Umbraco.Tests/Persistence/DatabaseContextTests.cs index 08dc2b8ca1..6981b16aba 100644 --- a/src/Umbraco.Tests/Persistence/DatabaseContextTests.cs +++ b/src/Umbraco.Tests/Persistence/DatabaseContextTests.cs @@ -38,7 +38,7 @@ namespace Umbraco.Tests.Persistence _sqlCeSyntaxProvider = new SqlCeSyntaxProvider(); _sqlSyntaxProviders = new[] { (ISqlSyntaxProvider) _sqlCeSyntaxProvider }; _logger = Mock.Of(); - var dbFactory = new DefaultDatabaseFactory(Core.Configuration.GlobalSettings.UmbracoConnectionName, _sqlSyntaxProviders, _logger, new TestUmbracoDatabaseAccessor(), Mock.Of()); + var dbFactory = new UmbracoDatabaseFactory(Core.Configuration.GlobalSettings.UmbracoConnectionName, _sqlSyntaxProviders, _logger, new TestUmbracoDatabaseAccessor(), Mock.Of()); _runtime = Mock.Of(); _migrationEntryService = Mock.Of(); _dbContext = new DatabaseContext(dbFactory); @@ -91,7 +91,7 @@ namespace Umbraco.Tests.Persistence } // re-create the database factory and database context with proper connection string - var dbFactory = new DefaultDatabaseFactory(connString, Constants.DbProviderNames.SqlCe, _sqlSyntaxProviders, _logger, new TestUmbracoDatabaseAccessor(), Mock.Of()); + var dbFactory = new UmbracoDatabaseFactory(connString, Constants.DbProviderNames.SqlCe, _sqlSyntaxProviders, _logger, new TestUmbracoDatabaseAccessor(), Mock.Of()); _dbContext = new DatabaseContext(dbFactory); // create application context diff --git a/src/Umbraco.Tests/Persistence/FaultHandling/ConnectionRetryTest.cs b/src/Umbraco.Tests/Persistence/FaultHandling/ConnectionRetryTest.cs index 5d24e066f1..402720f954 100644 --- a/src/Umbraco.Tests/Persistence/FaultHandling/ConnectionRetryTest.cs +++ b/src/Umbraco.Tests/Persistence/FaultHandling/ConnectionRetryTest.cs @@ -23,7 +23,7 @@ namespace Umbraco.Tests.Persistence.FaultHandling const string connectionString = @"server=.\SQLEXPRESS;database=EmptyForTest;user id=x;password=umbraco"; const string providerName = Constants.DbProviderNames.SqlServer; var sqlSyntax = new[] { new SqlServerSyntaxProvider(new Lazy(() => null)) }; - var factory = new DefaultDatabaseFactory(connectionString, providerName, sqlSyntax, Mock.Of(), new TestUmbracoDatabaseAccessor(), Mock.Of()); + var factory = new UmbracoDatabaseFactory(connectionString, providerName, sqlSyntax, Mock.Of(), new TestUmbracoDatabaseAccessor(), Mock.Of()); var database = factory.GetDatabase(); //Act @@ -38,7 +38,7 @@ namespace Umbraco.Tests.Persistence.FaultHandling const string connectionString = @"server=.\SQLEXPRESS;database=EmptyForTest;user id=umbraco;password=umbraco"; const string providerName = Constants.DbProviderNames.SqlServer; var sqlSyntax = new[] { new SqlServerSyntaxProvider(new Lazy(() => null)) }; - var factory = new DefaultDatabaseFactory(connectionString, providerName, sqlSyntax, Mock.Of(), new TestUmbracoDatabaseAccessor(), Mock.Of()); + var factory = new UmbracoDatabaseFactory(connectionString, providerName, sqlSyntax, Mock.Of(), new TestUmbracoDatabaseAccessor(), Mock.Of()); var database = factory.GetDatabase(); //Act diff --git a/src/Umbraco.Tests/Persistence/Repositories/ContentRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/ContentRepositoryTest.cs index 07aa8a1b1e..03e6bab17e 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/ContentRepositoryTest.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/ContentRepositoryTest.cs @@ -482,7 +482,7 @@ namespace Umbraco.Tests.Persistence.Repositories try { DatabaseContext.Database.EnableSqlTrace = true; - DatabaseContext.Database.EnableSqlCount(); + DatabaseContext.Database.EnableSqlCount = true; var result = repository.GetPagedResultsByQuery(query, 0, 2, out totalRecords, "title", Direction.Ascending, false); @@ -496,7 +496,7 @@ namespace Umbraco.Tests.Persistence.Repositories finally { DatabaseContext.Database.EnableSqlTrace = false; - DatabaseContext.Database.DisableSqlCount(); + DatabaseContext.Database.EnableSqlCount = false; } } } @@ -517,7 +517,7 @@ namespace Umbraco.Tests.Persistence.Repositories try { DatabaseContext.Database.EnableSqlTrace = true; - DatabaseContext.Database.EnableSqlCount(); + DatabaseContext.Database.EnableSqlCount = true; var result = repository.GetPagedResultsByQuery(query, 0, 1, out totalRecords, "Name", Direction.Ascending, true); // Assert @@ -528,7 +528,7 @@ namespace Umbraco.Tests.Persistence.Repositories finally { DatabaseContext.Database.EnableSqlTrace = false; - DatabaseContext.Database.DisableSqlCount(); + DatabaseContext.Database.EnableSqlCount = false; } } } diff --git a/src/Umbraco.Tests/Services/ContentServiceTests.cs b/src/Umbraco.Tests/Services/ContentServiceTests.cs index 7a6749d6a7..7e6236407c 100644 --- a/src/Umbraco.Tests/Services/ContentServiceTests.cs +++ b/src/Umbraco.Tests/Services/ContentServiceTests.cs @@ -1460,7 +1460,7 @@ namespace Umbraco.Tests.Services [Test] public void Can_Save_Lazy_Content() { - var databaseFactory = new DefaultDatabaseFactory( + var databaseFactory = new UmbracoDatabaseFactory( Umbraco.Core.Configuration.GlobalSettings.UmbracoConnectionName, TestObjects.GetDefaultSqlSyntaxProviders(Logger), Logger, diff --git a/src/Umbraco.Tests/Services/LocalizationServiceTests.cs b/src/Umbraco.Tests/Services/LocalizationServiceTests.cs index 4cb1909d81..917bab621e 100644 --- a/src/Umbraco.Tests/Services/LocalizationServiceTests.cs +++ b/src/Umbraco.Tests/Services/LocalizationServiceTests.cs @@ -129,7 +129,7 @@ namespace Umbraco.Tests.Services } DatabaseContext.Database.EnableSqlTrace = true; - DatabaseContext.Database.EnableSqlCount(); + DatabaseContext.Database.EnableSqlCount = true; var items = ServiceContext.LocalizationService.GetDictionaryItemDescendants(_parentItemGuidId) .ToArray(); @@ -143,7 +143,7 @@ namespace Umbraco.Tests.Services finally { DatabaseContext.Database.EnableSqlTrace = false; - DatabaseContext.Database.DisableSqlCount(); + DatabaseContext.Database.EnableSqlCount = false; } } diff --git a/src/Umbraco.Tests/Services/ThreadSafetyServiceTest.cs b/src/Umbraco.Tests/Services/ThreadSafetyServiceTest.cs index 55aa5e724b..e98e549cc3 100644 --- a/src/Umbraco.Tests/Services/ThreadSafetyServiceTest.cs +++ b/src/Umbraco.Tests/Services/ThreadSafetyServiceTest.cs @@ -229,13 +229,13 @@ namespace Umbraco.Tests.Services } /// - /// A special implementation of that mimics the DefaultDatabaseFactory + /// A special implementation of that mimics the UmbracoDatabaseFactory /// (one db per HttpContext) by providing one db per thread, as required for multi-threaded /// tests. /// internal class PerThreadSqlCeDatabaseFactory : DisposableObject, IDatabaseFactory { - // the DefaultDatabaseFactory uses thread-static databases where there is no http context, + // the UmbracoDatabaseFactory uses thread-static databases where there is no http context, // so it would need to be disposed in each thread in order for each database to be disposed, // instead we use this factory which also maintains one database per thread but can dispose // them all in one call diff --git a/src/Umbraco.Tests/TestHelpers/TestObjects.cs b/src/Umbraco.Tests/TestHelpers/TestObjects.cs index 25df624745..3c105adb73 100644 --- a/src/Umbraco.Tests/TestHelpers/TestObjects.cs +++ b/src/Umbraco.Tests/TestHelpers/TestObjects.cs @@ -226,7 +226,7 @@ namespace Umbraco.Tests.TestHelpers //mappersBuilder.AddCore(); //var mappers = mappersBuilder.CreateCollection(); var mappers = Current.Container.GetInstance(); - databaseFactory = new DefaultDatabaseFactory(GlobalSettings.UmbracoConnectionName, GetDefaultSqlSyntaxProviders(logger), logger, accessor, mappers); + databaseFactory = new UmbracoDatabaseFactory(GlobalSettings.UmbracoConnectionName, GetDefaultSqlSyntaxProviders(logger), logger, accessor, mappers); } repositoryFactory = repositoryFactory ?? new RepositoryFactory(Mock.Of()); return new NPocoUnitOfWorkProvider(new DatabaseContext(databaseFactory), repositoryFactory); diff --git a/src/Umbraco.Tests/TestHelpers/TestWithApplicationBase.cs b/src/Umbraco.Tests/TestHelpers/TestWithApplicationBase.cs index d6ffd97561..5fb61a56cd 100644 --- a/src/Umbraco.Tests/TestHelpers/TestWithApplicationBase.cs +++ b/src/Umbraco.Tests/TestHelpers/TestWithApplicationBase.cs @@ -121,7 +121,7 @@ namespace Umbraco.Tests.TestHelpers Container.RegisterSingleton(); var sqlSyntaxProviders = TestObjects.GetDefaultSqlSyntaxProviders(Logger); Container.RegisterSingleton(_ => sqlSyntaxProviders.OfType().First()); - Container.RegisterSingleton(f => new DefaultDatabaseFactory( + Container.RegisterSingleton(f => new UmbracoDatabaseFactory( Core.Configuration.GlobalSettings.UmbracoConnectionName, sqlSyntaxProviders, Logger, f.GetInstance(), diff --git a/src/Umbraco.Tests/TestHelpers/TestWithDatabaseBase.cs b/src/Umbraco.Tests/TestHelpers/TestWithDatabaseBase.cs index eafe08a960..e4defec7d8 100644 --- a/src/Umbraco.Tests/TestHelpers/TestWithDatabaseBase.cs +++ b/src/Umbraco.Tests/TestHelpers/TestWithDatabaseBase.cs @@ -42,7 +42,7 @@ namespace Umbraco.Tests.TestHelpers /// /// /// Can provide a SqlCE database populated with the Umbraco schema. The database should - /// be accessed through the DefaultDatabaseFactory. + /// be accessed through the UmbracoDatabaseFactory. /// Provides an Umbraco context and Xml content. /// fixme what else? /// @@ -100,7 +100,7 @@ namespace Umbraco.Tests.TestHelpers var logger = f.GetInstance(); var umbracoDatabaseAccessor = f.GetInstance(); var mappers = f.GetInstance(); - var factory = new DefaultDatabaseFactory(GetDbConnectionString(), GetDbProviderName(), sqlSyntaxProviders, logger, umbracoDatabaseAccessor, mappers); + var factory = new UmbracoDatabaseFactory(GetDbConnectionString(), GetDbProviderName(), sqlSyntaxProviders, logger, umbracoDatabaseAccessor, mappers); factory.ResetForTests(); return factory; }); diff --git a/src/Umbraco.Web.UI.Client/lib/umbraco/Extensions.js b/src/Umbraco.Web.UI.Client/lib/umbraco/Extensions.js index b70a6b12bc..3c148f0535 100644 --- a/src/Umbraco.Web.UI.Client/lib/umbraco/Extensions.js +++ b/src/Umbraco.Web.UI.Client/lib/umbraco/Extensions.js @@ -69,6 +69,22 @@ }; } + if (!String.prototype.htmlEncode) { + /** htmlEncode extension method for string */ + String.prototype.htmlEncode = function () { + //create a in-memory div, set it's inner text(which jQuery automatically encodes) + //then grab the encoded contents back out. The div never exists on the page. + return $('
').text(this).html(); + }; + } + + if (!String.prototype.htmlDecode) { + /** htmlDecode extension method for string */ + String.prototype.htmlDecode = function () { + return $('
').html(this).text(); + }; + } + if (!String.prototype.startsWith) { /** startsWith extension method for string */ String.prototype.startsWith = function (str) { diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/tags/tags.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/tags/tags.controller.js index d18ff73bd5..eb469cf610 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/tags/tags.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/tags/tags.controller.js @@ -191,4 +191,4 @@ angular.module("umbraco") }); } -); \ No newline at end of file +); diff --git a/src/Umbraco.Web.UI/config/splashes/noNodes.aspx b/src/Umbraco.Web.UI/config/splashes/noNodes.aspx index 92bf10650c..d46ecd6113 100644 --- a/src/Umbraco.Web.UI/config/splashes/noNodes.aspx +++ b/src/Umbraco.Web.UI/config/splashes/noNodes.aspx @@ -44,7 +44,7 @@

Be a part of the community

-

The Umbraco community is the best of its kind, be sure to visit, and if you have any questions, we’re sure that you can get your answers from the community.

+

The Umbraco community is the best of its kind, be sure to visit, and if you have any questions, we're sure that you can get your answers from the community.

our.Umbraco →
diff --git a/src/Umbraco.Web/BatchedDatabaseServerMessenger.cs b/src/Umbraco.Web/BatchedDatabaseServerMessenger.cs index cf85e65f71..49f9592d1f 100644 --- a/src/Umbraco.Web/BatchedDatabaseServerMessenger.cs +++ b/src/Umbraco.Web/BatchedDatabaseServerMessenger.cs @@ -62,7 +62,7 @@ namespace Umbraco.Web } } - private void UmbracoModule_EndRequest(object sender, EventArgs e) + private void UmbracoModule_EndRequest(object sender, UmbracoRequestEventArgs e) { // will clear the batch - will remain in HttpContext though - that's ok FlushBatch(); diff --git a/src/Umbraco.Web/BatchedWebServiceServerMessenger.cs b/src/Umbraco.Web/BatchedWebServiceServerMessenger.cs index 0fe3464140..766683a1c9 100644 --- a/src/Umbraco.Web/BatchedWebServiceServerMessenger.cs +++ b/src/Umbraco.Web/BatchedWebServiceServerMessenger.cs @@ -65,7 +65,7 @@ namespace Umbraco.Web return batch; } - private void UmbracoModule_EndRequest(object sender, EventArgs e) + private void UmbracoModule_EndRequest(object sender, UmbracoRequestEventArgs e) { FlushBatch(); } diff --git a/src/Umbraco.Web/Editors/MediaTypeController.cs b/src/Umbraco.Web/Editors/MediaTypeController.cs index 31194aee68..2927339ffd 100644 --- a/src/Umbraco.Web/Editors/MediaTypeController.cs +++ b/src/Umbraco.Web/Editors/MediaTypeController.cs @@ -31,6 +31,7 @@ namespace Umbraco.Web.Editors return Services.ContentTypeService.Count(); } + [UmbracoTreeAuthorize(Constants.Trees.MediaTypes, Constants.Trees.Media)] public MediaTypeDisplay GetById(int id) { var ct = Services.MediaTypeService.Get(id); diff --git a/src/Umbraco.Web/HybridDatabaseScopeAccessor.cs b/src/Umbraco.Web/HybridDatabaseScopeAccessor.cs new file mode 100644 index 0000000000..008fddc3ef --- /dev/null +++ b/src/Umbraco.Web/HybridDatabaseScopeAccessor.cs @@ -0,0 +1,19 @@ +using Umbraco.Core.Persistence; + +namespace Umbraco.Web +{ + internal class HybridDatabaseScopeAccessor : HybridAccessorBase, IDatabaseScopeAccessor + { + protected override string ItemKey => "Umbraco.Core.Persistence.HybridDatabaseScopeAccessor"; + + public HybridDatabaseScopeAccessor(IHttpContextAccessor httpContextAccessor) + : base(httpContextAccessor) + { } + + public DatabaseScope Scope + { + get { return Value; } + set { Value = value; } + } + } +} diff --git a/src/Umbraco.Web/PropertyEditors/TagsPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/TagsPropertyEditor.cs index e0cddceb45..b30e58b72d 100644 --- a/src/Umbraco.Web/PropertyEditors/TagsPropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/TagsPropertyEditor.cs @@ -1,6 +1,7 @@ using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Linq; +using System.Net; using System.Runtime.InteropServices; using Newtonsoft.Json.Linq; using Umbraco.Core; @@ -61,7 +62,14 @@ namespace Umbraco.Web.PropertyEditors public override object ConvertEditorToDb(ContentPropertyData editorValue, object currentValue) { var json = editorValue.Value as JArray; - return json == null ? null : json.Select(x => x.Value()); + return json == null + ? null + : json.Select(x => x.Value()).Where(x => x.IsNullOrWhiteSpace() == false) + //First we will decode it as html because we know that if this is not a malicious post that the value is + // already Html encoded by the tags JavaScript controller. Then we'll re-Html Encode it to ensure that in case this + // is a malicious post (i.e. someone is submitting data manually by modifying the request). + .Select(WebUtility.HtmlDecode) + .Select(WebUtility.HtmlEncode); } /// diff --git a/src/Umbraco.Web/PublishedCache/XmlPublishedCache/PublishedContentCache.cs b/src/Umbraco.Web/PublishedCache/XmlPublishedCache/PublishedContentCache.cs index 803477f6e8..9191c4b97e 100644 --- a/src/Umbraco.Web/PublishedCache/XmlPublishedCache/PublishedContentCache.cs +++ b/src/Umbraco.Web/PublishedCache/XmlPublishedCache/PublishedContentCache.cs @@ -207,7 +207,7 @@ namespace Umbraco.Web.PublishedCache.XmlPublishedCache elt = null; var min = int.MaxValue; - foreach (XmlElement e in xml.DocumentElement.ChildNodes) + foreach (var e in xml.DocumentElement.ChildNodes.OfType()) { var sortOrder = int.Parse(e.GetAttribute("sortOrder")); if (sortOrder < min) @@ -229,7 +229,7 @@ namespace Umbraco.Web.PublishedCache.XmlPublishedCache if (hideTopLevelNode && startNodeId <= 0) { - foreach (XmlElement e in elt.ChildNodes) + foreach (var e in elt.ChildNodes.OfType()) { var id = NavigateElementRoute(e, urlParts); if (id > 0) return id; @@ -240,14 +240,14 @@ namespace Umbraco.Web.PublishedCache.XmlPublishedCache return NavigateElementRoute(elt, urlParts); } - private int NavigateElementRoute(XmlElement elt, string[] urlParts) + private static int NavigateElementRoute(XmlElement elt, string[] urlParts) { var found = true; var i = 0; while (found && i < urlParts.Length) { found = false; - foreach (XmlElement child in elt.ChildNodes) + foreach (var child in elt.ChildNodes.OfType()) { var noNode = child.GetAttributeNode("isDoc") == null; if (noNode) continue; diff --git a/src/Umbraco.Web/Scheduling/LogScrubber.cs b/src/Umbraco.Web/Scheduling/LogScrubber.cs index 1daf4ba691..65c7df101e 100644 --- a/src/Umbraco.Web/Scheduling/LogScrubber.cs +++ b/src/Umbraco.Web/Scheduling/LogScrubber.cs @@ -16,14 +16,16 @@ namespace Umbraco.Web.Scheduling private readonly IUmbracoSettingsSection _settings; private readonly ILogger _logger; private readonly ProfilingLogger _proflog; + private readonly DatabaseContext _databaseContext; public LogScrubber(IBackgroundTaskRunner runner, int delayMilliseconds, int periodMilliseconds, - IRuntimeState runtime, IAuditService auditService, IUmbracoSettingsSection settings, ILogger logger, ProfilingLogger proflog) + IRuntimeState runtime, IAuditService auditService, IUmbracoSettingsSection settings, DatabaseContext databaseContext, ILogger logger, ProfilingLogger proflog) : base(runner, delayMilliseconds, periodMilliseconds) { _runtime = runtime; _auditService = auditService; _settings = settings; + _databaseContext = databaseContext; _logger = logger; _proflog = proflog; } @@ -79,6 +81,8 @@ namespace Umbraco.Web.Scheduling return false; // do NOT repeat, going down } + // running on a background task, requires a database scope + using (_databaseContext.CreateDatabaseScope()) using (_proflog.DebugDuration("Log scrubbing executing", "Log scrubbing complete")) { _auditService.CleanLogs(GetLogScrubbingMaximumAge(_settings)); diff --git a/src/Umbraco.Web/Scheduling/ScheduledPublishing.cs b/src/Umbraco.Web/Scheduling/ScheduledPublishing.cs index bcb36bfd72..6894e267d8 100644 --- a/src/Umbraco.Web/Scheduling/ScheduledPublishing.cs +++ b/src/Umbraco.Web/Scheduling/ScheduledPublishing.cs @@ -14,15 +14,17 @@ namespace Umbraco.Web.Scheduling { private readonly IRuntimeState _runtime; private readonly IUserService _userService; + private readonly DatabaseContext _databaseContext; private readonly ILogger _logger; private readonly ProfilingLogger _proflog; public ScheduledPublishing(IBackgroundTaskRunner runner, int delayMilliseconds, int periodMilliseconds, - IRuntimeState runtime, IUserService userService, ILogger logger, ProfilingLogger proflog) + IRuntimeState runtime, IUserService userService, DatabaseContext databaseContext, ILogger logger, ProfilingLogger proflog) : base(runner, delayMilliseconds, periodMilliseconds) { _runtime = runtime; _userService = userService; + _databaseContext = databaseContext; _logger = logger; _proflog = proflog; } @@ -80,8 +82,13 @@ namespace Umbraco.Web.Scheduling Content = new StringContent(string.Empty) }; - //pass custom the authorization header + // running on a background task, requires a database scope + // (GetAuthenticationHeaderValue uses UserService to load the current user, hence requires a database) + using (_databaseContext.CreateDatabaseScope()) + { + //pass custom the authorization header request.Headers.Authorization = AdminTokenAuthorizeAttribute.GetAuthenticationHeaderValue(_userService); + } var result = await wc.SendAsync(request, token); } @@ -97,7 +104,6 @@ namespace Umbraco.Web.Scheduling public override bool IsAsync => true; - public override bool RunsOnShutdown => false; } } \ No newline at end of file diff --git a/src/Umbraco.Web/Scheduling/SchedulerComponent.cs b/src/Umbraco.Web/Scheduling/SchedulerComponent.cs index 2689742e14..fb19591047 100644 --- a/src/Umbraco.Web/Scheduling/SchedulerComponent.cs +++ b/src/Umbraco.Web/Scheduling/SchedulerComponent.cs @@ -13,7 +13,7 @@ namespace Umbraco.Web.Scheduling /// Used to do the scheduling for tasks, publishing, etc... /// /// - /// All tasks are run in a background task runner which is web aware and will wind down + /// All tasks are run in a background task runner which is web aware and will wind down /// the task correctly instead of killing it completely when the app domain shuts down. /// [RuntimeLevel(MinLevel = RuntimeLevel.Run)] @@ -24,6 +24,7 @@ namespace Umbraco.Web.Scheduling private IAuditService _auditService; private ILogger _logger; private ProfilingLogger _proflog; + private DatabaseContext _databaseContext; private BackgroundTaskRunner _keepAliveRunner; private BackgroundTaskRunner _publishingRunner; @@ -34,11 +35,12 @@ namespace Umbraco.Web.Scheduling private object _locker = new object(); private IBackgroundTask[] _tasks; - public void Initialize(IRuntimeState runtime, IUserService userService, IAuditService auditService, ILogger logger, ProfilingLogger proflog) + public void Initialize(IRuntimeState runtime, IUserService userService, IAuditService auditService, DatabaseContext databaseContext, ILogger logger, ProfilingLogger proflog) { _runtime = runtime; _userService = userService; _auditService = auditService; + _databaseContext = databaseContext; _logger = logger; _proflog = proflog; @@ -76,9 +78,9 @@ namespace Umbraco.Web.Scheduling var tasks = new List { new KeepAlive(_keepAliveRunner, 60000, 300000, _runtime, _logger, _proflog), - new ScheduledPublishing(_publishingRunner, 60000, 60000, _runtime, _userService, _logger, _proflog), + new ScheduledPublishing(_publishingRunner, 60000, 60000, _runtime, _userService, _databaseContext, _logger, _proflog), new ScheduledTasks(_tasksRunner, 60000, 60000, _runtime, settings, _logger, _proflog), - new LogScrubber(_scrubberRunner, 60000, LogScrubber.GetLogScrubbingInterval(settings, _logger), _runtime, _auditService, settings, _logger, _proflog) + new LogScrubber(_scrubberRunner, 60000, LogScrubber.GetLogScrubbingInterval(settings, _logger), _runtime, _auditService, settings, _databaseContext, _logger, _proflog) }; // ping/keepalive diff --git a/src/Umbraco.Web/Search/ExamineComponent.cs b/src/Umbraco.Web/Search/ExamineComponent.cs index 274005b4da..d08d87a32a 100644 --- a/src/Umbraco.Web/Search/ExamineComponent.cs +++ b/src/Umbraco.Web/Search/ExamineComponent.cs @@ -34,6 +34,7 @@ namespace Umbraco.Web.Search logger.Info("Starting initialize async background thread."); // make it async in order not to slow down the boot + // fixme - should be a proper background task else we cannot stop it! var bg = new Thread(() => { try @@ -83,7 +84,11 @@ namespace Umbraco.Web.Search MediaCacheRefresher.CacheUpdated += MediaCacheRefresherUpdated; MemberCacheRefresher.CacheUpdated += MemberCacheRefresherUpdated; - var contentIndexer = ExamineManager.Instance.IndexProviderCollection[Constants.Examine.InternalIndexer] as UmbracoContentIndexer; + // fixme - content type? + // events handling removed in ef013f9d3b945d0a48a306ff1afbd49c10c3fff8 + // because, could not make sense of it? + + var contentIndexer = ExamineManager.Instance.IndexProviderCollection[Constants.Examine.InternalIndexer] as UmbracoContentIndexer; if (contentIndexer != null) { contentIndexer.DocumentWriting += IndexerDocumentWriting; @@ -104,7 +109,6 @@ namespace Umbraco.Web.Search indexer.Value.RebuildIndex(); } - private static void BindGridToExamine(GridPropertyEditor grid, IExamineIndexCollectionAccessor indexCollection) { var indexes = indexCollection.Indexes; diff --git a/src/Umbraco.Web/Security/MembershipHelper.cs b/src/Umbraco.Web/Security/MembershipHelper.cs index 0bf3ef776d..64816b0254 100644 --- a/src/Umbraco.Web/Security/MembershipHelper.cs +++ b/src/Umbraco.Web/Security/MembershipHelper.cs @@ -398,7 +398,8 @@ namespace Umbraco.Web.Security var viewProperties = new List(); foreach (var prop in memberType.PropertyTypes - .Where(x => builtIns.Contains(x.Alias) == false && memberType.MemberCanEditProperty(x.Alias))) + .Where(x => builtIns.Contains(x.Alias) == false && memberType.MemberCanEditProperty(x.Alias)) + .OrderBy(p => p.SortOrder)) { var value = string.Empty; if (member != null) diff --git a/src/Umbraco.Web/Strategies/DatabaseServerRegistrarAndMessengerComponent.cs b/src/Umbraco.Web/Strategies/DatabaseServerRegistrarAndMessengerComponent.cs index 0de0d6f129..fc6058f68c 100644 --- a/src/Umbraco.Web/Strategies/DatabaseServerRegistrarAndMessengerComponent.cs +++ b/src/Umbraco.Web/Strategies/DatabaseServerRegistrarAndMessengerComponent.cs @@ -39,6 +39,7 @@ namespace Umbraco.Web.Strategies private BackgroundTaskRunner _backgroundTaskRunner; private bool _started; private TouchServerTask _task; + private DatabaseContext _databaseContext; public override void Compose(Composition composition) { @@ -94,7 +95,7 @@ namespace Umbraco.Web.Strategies indexer.Value.RebuildIndex(); } - public void Initialize(IRuntimeState runtime, IServerRegistrar serverRegistrar, IServerRegistrationService registrationService, ILogger logger) + public void Initialize(IRuntimeState runtime, IServerRegistrar serverRegistrar, IServerRegistrationService registrationService, DatabaseContext databaseContext, ILogger logger) { if (UmbracoConfig.For.UmbracoSettings().DistributedCall.Enabled) return; @@ -102,6 +103,7 @@ namespace Umbraco.Web.Strategies if (_registrar == null) throw new Exception("panic: registar."); _runtime = runtime; + _databaseContext = databaseContext; _logger = logger; _registrationService = registrationService; @@ -131,10 +133,10 @@ namespace Umbraco.Web.Strategies case EnsureRoutableOutcome.IsRoutable: case EnsureRoutableOutcome.NotDocumentRequest: RegisterBackgroundTasks(e); - break; + break; } } - + private void RegisterBackgroundTasks(UmbracoRequestEventArgs e) { // remove handler, we're done @@ -149,8 +151,8 @@ namespace Umbraco.Web.Strategies var task = new TouchServerTask(_backgroundTaskRunner, 15000, //delay before first execution _registrar.Options.RecurringSeconds*1000, //amount of ms between executions - svc, _registrar, serverAddress, _logger); - + svc, _registrar, serverAddress, _databaseContext, _logger); + // perform the rest async, we don't want to block the startup sequence // this will just reoccur on a background thread _backgroundTaskRunner.TryAdd(task); @@ -164,6 +166,7 @@ namespace Umbraco.Web.Strategies private readonly IServerRegistrationService _svc; private readonly DatabaseServerRegistrar _registrar; private readonly string _serverAddress; + private readonly DatabaseContext _databaseContext; private readonly ILogger _logger; /// @@ -175,16 +178,18 @@ namespace Umbraco.Web.Strategies /// /// /// + /// /// /// The task will repeat itself periodically. Use this constructor to create a new task. public TouchServerTask(IBackgroundTaskRunner runner, int delayMilliseconds, int periodMilliseconds, - IServerRegistrationService svc, DatabaseServerRegistrar registrar, string serverAddress, ILogger logger) + IServerRegistrationService svc, DatabaseServerRegistrar registrar, string serverAddress, DatabaseContext databaseContext, ILogger logger) : base(runner, delayMilliseconds, periodMilliseconds) { if (svc == null) throw new ArgumentNullException(nameof(svc)); _svc = svc; _registrar = registrar; _serverAddress = serverAddress; + _databaseContext = databaseContext; _logger = logger; } @@ -200,7 +205,11 @@ namespace Umbraco.Web.Strategies { try { - _svc.TouchServer(_serverAddress, _svc.CurrentServerIdentity, _registrar.Options.StaleServerTimeout); + // running on a background task, requires a database scope + using (_databaseContext.CreateDatabaseScope()) + { + _svc.TouchServer(_serverAddress, _svc.CurrentServerIdentity, _registrar.Options.StaleServerTimeout); + } return true; // repeat } catch (Exception ex) diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index e388f73a59..89d3c0d4f1 100644 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -167,6 +167,7 @@ + diff --git a/src/Umbraco.Web/UmbracoModule.cs b/src/Umbraco.Web/UmbracoModule.cs index e2b29bdbcd..97e986d8f3 100644 --- a/src/Umbraco.Web/UmbracoModule.cs +++ b/src/Umbraco.Web/UmbracoModule.cs @@ -550,7 +550,7 @@ namespace Umbraco.Web OnEndRequest(new UmbracoRequestEventArgs(UmbracoContext.Current, new HttpContextWrapper(httpContext))); - DisposeHttpContextItems(httpContext); + DisposeHttpContextItems(httpContext); }; } @@ -568,9 +568,9 @@ namespace Umbraco.Web RouteAttempt?.Invoke(this, args); } - public static event EventHandler EndRequest; + public static event EventHandler EndRequest; - private void OnEndRequest(EventArgs args) + private void OnEndRequest(UmbracoRequestEventArgs args) { EndRequest?.Invoke(this, args); } diff --git a/src/UmbracoExamine/BaseUmbracoIndexer.cs b/src/UmbracoExamine/BaseUmbracoIndexer.cs index 5365d992a5..b886a4c419 100644 --- a/src/UmbracoExamine/BaseUmbracoIndexer.cs +++ b/src/UmbracoExamine/BaseUmbracoIndexer.cs @@ -32,6 +32,12 @@ namespace UmbracoExamine /// public abstract class BaseUmbracoIndexer : LuceneIndexer { + // note + // wrapping all operations that end up calling base.SafelyProcessQueueItems in a safe call + // context because they will fork a thread/task/whatever which should *not* capture our + // call context (and the database it can contain)! ideally we should be able to override + // SafelyProcessQueueItems but that's not possible in the current version of Examine. + /// /// Used to store the path of a content object /// @@ -232,7 +238,10 @@ namespace UmbracoExamine if (CanInitialize()) { ProfilingLogger.Logger.Debug(GetType(), "Rebuilding index"); - base.RebuildIndex(); + using (new SafeCallContext()) + { + base.RebuildIndex(); + } } } @@ -246,7 +255,10 @@ namespace UmbracoExamine { if (CanInitialize()) { - base.IndexAll(type); + using (new SafeCallContext()) + { + base.IndexAll(type); + } } } @@ -254,7 +266,10 @@ namespace UmbracoExamine { if (CanInitialize()) { - base.IndexItems(nodes); + using (new SafeCallContext()) + { + base.IndexItems(nodes); + } } } @@ -269,7 +284,10 @@ namespace UmbracoExamine if (node.Attribute("id") != null) { ProfilingLogger.Logger.Debug(GetType(), "ReIndexNode {0} with type {1}", () => node.Attribute("id"), () => type); - base.ReIndexNode(node, type); + using (new SafeCallContext()) + { + base.ReIndexNode(node, type); + } } else { @@ -289,7 +307,10 @@ namespace UmbracoExamine { if (CanInitialize()) { - base.DeleteFromIndex(nodeId); + using (new SafeCallContext()) + { + base.DeleteFromIndex(nodeId); + } } } diff --git a/src/UmbracoExamine/DataServices/UmbracoContentService.cs b/src/UmbracoExamine/DataServices/UmbracoContentService.cs index 5c3b5103e7..71bbcc0452 100644 --- a/src/UmbracoExamine/DataServices/UmbracoContentService.cs +++ b/src/UmbracoExamine/DataServices/UmbracoContentService.cs @@ -21,6 +21,8 @@ //using System.Data.SqlClient; //using System.Diagnostics; +//// FIXME WOULD NEED TO WRAP THE WHOLE THING IN DATABASE SCOPE (see 7.6) + //namespace UmbracoExamine.DataServices //{ // public class UmbracoContentService diff --git a/src/umbraco.cms/businesslogic/CMSNode.cs b/src/umbraco.cms/businesslogic/CMSNode.cs index 3f4351e5a8..fb76689cdd 100644 --- a/src/umbraco.cms/businesslogic/CMSNode.cs +++ b/src/umbraco.cms/businesslogic/CMSNode.cs @@ -985,25 +985,24 @@ order by level,sortOrder"; protected virtual XmlNode GetPreviewXml(XmlDocument xd, Guid version) { + var xmlDoc = new XmlDocument(); - XmlDocument xmlDoc = new XmlDocument(); - using (var sqlHelper = LegacySqlHelper.SqlHelper) - using (XmlReader xmlRdr = sqlHelper.ExecuteXmlReader( - "select xml from cmsPreviewXml where nodeID = @nodeId and versionId = @versionId", - sqlHelper.CreateParameter("@nodeId", Id), - sqlHelper.CreateParameter("@versionId", version))) - { - xmlDoc.Load(xmlRdr); - } + var xmlStr = Current.DatabaseContext.Database.ExecuteScalar( + "select xml from cmsPreviewXml where nodeID = @nodeId and versionId = @versionId", + new { nodeId = Id, versionId = version }); + if (xmlStr.IsNullOrWhiteSpace()) return null; + + xmlDoc.LoadXml(xmlStr); + return xd.ImportNode(xmlDoc.FirstChild, true); } protected internal virtual bool PreviewExists(Guid versionId) { - using (var sqlHelper = LegacySqlHelper.SqlHelper) - return sqlHelper.ExecuteScalar("SELECT COUNT(nodeId) FROM cmsPreviewXml WHERE nodeId=@nodeId and versionId = @versionId", - sqlHelper.CreateParameter("@nodeId", Id), sqlHelper.CreateParameter("@versionId", versionId)) != 0; + return Current.DatabaseContext.Database.ExecuteScalar( + "SELECT COUNT(nodeId) FROM cmsPreviewXml WHERE nodeId=@nodeId and versionId = @versionId", + new {nodeId = Id, versionId = versionId}) != 0; } @@ -1018,12 +1017,9 @@ order by level,sortOrder"; var sql = PreviewExists(versionId) ? "UPDATE cmsPreviewXml SET xml = @xml, timestamp = @timestamp WHERE nodeId=@nodeId AND versionId = @versionId" : "INSERT INTO cmsPreviewXml(nodeId, versionId, timestamp, xml) VALUES (@nodeId, @versionId, @timestamp, @xml)"; - using (var sqlHelper = LegacySqlHelper.SqlHelper) - sqlHelper.ExecuteNonQuery(sql, - sqlHelper.CreateParameter("@nodeId", Id), - sqlHelper.CreateParameter("@versionId", versionId), - sqlHelper.CreateParameter("@timestamp", DateTime.Now), - sqlHelper.CreateParameter("@xml", x.OuterXml)); + Current.DatabaseContext.Database.Execute( + sql, new {nodeId = Id, versionId = versionId, timestamp = DateTime.Now, xml = x.OuterXml}); + } protected void PopulateCMSNodeFromReader(IRecordsReader dr)