diff --git a/src/Umbraco.Core/Persistence/LocalDb.cs b/src/Umbraco.Core/Persistence/LocalDb.cs index 7b5ea196ea..995508ecf7 100644 --- a/src/Umbraco.Core/Persistence/LocalDb.cs +++ b/src/Umbraco.Core/Persistence/LocalDb.cs @@ -24,6 +24,7 @@ namespace Umbraco.Core.Persistence { private int _version; private bool _hasVersion; + private string _exe; #region Availability & Version @@ -84,16 +85,31 @@ namespace Umbraco.Core.Persistence { _hasVersion = true; _version = -1; + _exe = null; var programFiles = Environment.GetEnvironmentVariable("ProgramFiles"); - if (programFiles == null) return; + + // MS SQL Server installs in e.g. "C:\Program Files\Microsoft SQL Server", so + // we want to detect it in "%ProgramFiles%\Microsoft SQL Server" - however, if + // Umbraco runs as a 32bits process (e.g. IISExpress configured as 32bits) + // on a 64bits system, %ProgramFiles% will point to "C:\Program Files (x86)" + // and SQL Server cannot be found. But then, %ProgramW6432% will point to + // the original "C:\Program Files". Using it to fix the path. + // see also: MSDN doc for WOW64 implementation + // + var programW6432 = Environment.GetEnvironmentVariable("ProgramW6432"); + if (string.IsNullOrWhiteSpace(programW6432) == false && programW6432 != programFiles) + programFiles = programW6432; + + if (string.IsNullOrWhiteSpace(programFiles)) return; // detect 14, 13, 12, 11 for (var i = 14; i > 10; i--) { - var path = Path.Combine(programFiles, string.Format(@"Microsoft SQL Server\{0}0\Tools\Binn\SqlLocalDB.exe", i)); - if (File.Exists(path) == false) continue; + var exe = Path.Combine(programFiles, $@"Microsoft SQL Server\{i}0\Tools\Binn\SqlLocalDB.exe"); + if (File.Exists(exe) == false) continue; _version = i; + _exe = exe; break; } } @@ -110,8 +126,7 @@ namespace Umbraco.Core.Persistence public string[] GetInstances() { EnsureAvailable(); - string output, error; - var rc = ExecuteSqlLocalDb("i", out output, out error); // info + var rc = ExecuteSqlLocalDb("i", out var output, out var error); // info if (rc != 0 || error != string.Empty) return null; return output.Split(new[] { Environment.NewLine }, StringSplitOptions.RemoveEmptyEntries); } @@ -138,8 +153,7 @@ namespace Umbraco.Core.Persistence public bool CreateInstance(string instanceName) { EnsureAvailable(); - string output, error; - return ExecuteSqlLocalDb(string.Format("c \"{0}\"", instanceName), out output, out error) == 0 && error == string.Empty; + return ExecuteSqlLocalDb($"c \"{instanceName}\"", out _, out var error) == 0 && error == string.Empty; } /// @@ -160,9 +174,8 @@ namespace Umbraco.Core.Persistence instance.DropDatabases(); // else the files remain // -i force NOWAIT, -k kills - string output, error; - return ExecuteSqlLocalDb(string.Format("p \"{0}\" -i", instanceName), out output, out error) == 0 && error == string.Empty - && ExecuteSqlLocalDb(string.Format("d \"{0}\"", instanceName), out output, out error) == 0 && error == string.Empty; + return ExecuteSqlLocalDb($"p \"{instanceName}\" -i", out _, out var error) == 0 && error == string.Empty + && ExecuteSqlLocalDb($"d \"{instanceName}\"", out _, out error) == 0 && error == string.Empty; } /// @@ -180,8 +193,7 @@ namespace Umbraco.Core.Persistence if (InstanceExists(instanceName) == false) return true; // -i force NOWAIT, -k kills - string output, error; - return ExecuteSqlLocalDb(string.Format("p \"{0}\" -i", instanceName), out output, out error) == 0 && error == string.Empty; + return ExecuteSqlLocalDb($"p \"{instanceName}\" -i", out _, out var error) == 0 && error == string.Empty; } /// @@ -197,8 +209,7 @@ namespace Umbraco.Core.Persistence { EnsureAvailable(); if (InstanceExists(instanceName) == false) return false; - string output, error; - return ExecuteSqlLocalDb(string.Format("s \"{0}\"", instanceName), out output, out error) == 0 && error == string.Empty; + return ExecuteSqlLocalDb($"s \"{instanceName}\"", out _, out var error) == 0 && error == string.Empty; } /// @@ -230,7 +241,7 @@ namespace Umbraco.Core.Persistence /// /// Gets the name of the instance. /// - public string InstanceName { get; private set; } + public string InstanceName { get; } /// /// Initializes a new instance of the class. @@ -239,7 +250,7 @@ namespace Umbraco.Core.Persistence public Instance(string instanceName) { InstanceName = instanceName; - _masterCstr = string.Format(@"Server=(localdb)\{0};Integrated Security=True;", instanceName); + _masterCstr = $@"Server=(localdb)\{instanceName};Integrated Security=True;"; } /// @@ -252,7 +263,7 @@ namespace Umbraco.Core.Persistence /// public string GetConnectionString(string databaseName) { - return _masterCstr + string.Format(@"Database={0};", databaseName); + return _masterCstr + $@"Database={databaseName};"; } /// @@ -268,10 +279,9 @@ namespace Umbraco.Core.Persistence /// public string GetAttachedConnectionString(string databaseName, string filesPath) { - string logName, baseFilename, baseLogFilename, mdfFilename, ldfFilename; - GetDatabaseFiles(databaseName, filesPath, out logName, out baseFilename, out baseLogFilename, out mdfFilename, out ldfFilename); + GetDatabaseFiles(databaseName, filesPath, out _, out _, out _, out var mdfFilename, out _); - return _masterCstr + string.Format(@"AttachDbFileName='{0}';", mdfFilename); + return _masterCstr + $@"AttachDbFileName='{mdfFilename}';"; } /// @@ -367,8 +377,7 @@ namespace Umbraco.Core.Persistence /// public bool CreateDatabase(string databaseName, string filesPath) { - string logName, baseFilename, baseLogFilename, mdfFilename, ldfFilename; - GetDatabaseFiles(databaseName, filesPath, out logName, out baseFilename, out baseLogFilename, out mdfFilename, out ldfFilename); + GetDatabaseFiles(databaseName, filesPath, out var logName, out _, out _, out var mdfFilename, out var ldfFilename); using (var conn = new SqlConnection(_masterCstr)) using (var cmd = conn.CreateCommand()) @@ -380,13 +389,10 @@ namespace Umbraco.Core.Persistence // cannot use parameters on CREATE DATABASE // ie "CREATE DATABASE @0 ..." does not work - SetCommand(cmd, string.Format(@" - CREATE DATABASE {0} - ON (NAME=N{1}, FILENAME={2}) - LOG ON (NAME=N{3}, FILENAME={4})", - QuotedName(databaseName), - QuotedName(databaseName, '\''), QuotedName(mdfFilename, '\''), - QuotedName(logName, '\''), QuotedName(ldfFilename, '\''))); + SetCommand(cmd, $@" + CREATE DATABASE {QuotedName(databaseName)} + ON (NAME=N{QuotedName(databaseName, '\'')}, FILENAME={QuotedName(mdfFilename, '\'')}) + LOG ON (NAME=N{QuotedName(logName, '\'')}, FILENAME={QuotedName(ldfFilename, '\'')})"); var unused = cmd.ExecuteNonQuery(); } @@ -608,9 +614,8 @@ namespace Umbraco.Core.Persistence { // cannot use parameters on ALTER DATABASE // ie "ALTER DATABASE @0 ..." does not work - SetCommand(cmd, string.Format(@" - ALTER DATABASE {0} SET SINGLE_USER WITH ROLLBACK IMMEDIATE", - QuotedName(databaseName))); + SetCommand(cmd, $@" + ALTER DATABASE {QuotedName(databaseName)} SET SINGLE_USER WITH ROLLBACK IMMEDIATE"); var unused1 = cmd.ExecuteNonQuery(); } @@ -631,9 +636,8 @@ namespace Umbraco.Core.Persistence // cannot use parameters on DROP DATABASE // ie "DROP DATABASE @0 ..." does not work - SetCommand(cmd, string.Format(@" - DROP DATABASE {0}", - QuotedName(databaseName))); + SetCommand(cmd, $@" + DROP DATABASE {QuotedName(databaseName)}"); var unused2 = cmd.ExecuteNonQuery(); @@ -651,7 +655,7 @@ namespace Umbraco.Core.Persistence private static string GetLogFilename(string mdfFilename) { if (mdfFilename.EndsWith(".mdf") == false) - throw new ArgumentException("Not a valid MDF filename (no .mdf extension).", "mdfFilename"); + throw new ArgumentException("Not a valid MDF filename (no .mdf extension).", nameof(mdfFilename)); return mdfFilename.Substring(0, mdfFilename.Length - ".mdf".Length) + "_log.ldf"; } @@ -664,9 +668,8 @@ namespace Umbraco.Core.Persistence { // cannot use parameters on ALTER DATABASE // ie "ALTER DATABASE @0 ..." does not work - SetCommand(cmd, string.Format(@" - ALTER DATABASE {0} SET SINGLE_USER WITH ROLLBACK IMMEDIATE", - QuotedName(databaseName))); + SetCommand(cmd, $@" + ALTER DATABASE {QuotedName(databaseName)} SET SINGLE_USER WITH ROLLBACK IMMEDIATE"); var unused1 = cmd.ExecuteNonQuery(); @@ -685,19 +688,16 @@ namespace Umbraco.Core.Persistence /// The directory containing database files. private static void AttachDatabase(SqlCommand cmd, string databaseName, string filesPath) { - string logName, baseFilename, baseLogFilename, mdfFilename, ldfFilename; - GetDatabaseFiles(databaseName, filesPath, out logName, out baseFilename, out baseLogFilename, out mdfFilename, out ldfFilename); + GetDatabaseFiles(databaseName, filesPath, + out var logName, out _, out _, out var mdfFilename, out var ldfFilename); // cannot use parameters on CREATE DATABASE // ie "CREATE DATABASE @0 ..." does not work - SetCommand(cmd, string.Format(@" - CREATE DATABASE {0} - ON (NAME=N{1}, FILENAME={2}) - LOG ON (NAME=N{3}, FILENAME={4}) - FOR ATTACH", - QuotedName(databaseName), - QuotedName(databaseName, '\''), QuotedName(mdfFilename, '\''), - QuotedName(logName, '\''), QuotedName(ldfFilename, '\''))); + SetCommand(cmd, $@" + CREATE DATABASE {QuotedName(databaseName)} + ON (NAME=N{QuotedName(databaseName, '\'')}, FILENAME={QuotedName(mdfFilename, '\'')}) + LOG ON (NAME=N{QuotedName(logName, '\'')}, FILENAME={QuotedName(ldfFilename, '\'')}) + FOR ATTACH"); var unused = cmd.ExecuteNonQuery(); } @@ -790,9 +790,8 @@ namespace Umbraco.Core.Persistence && (sourceExtension == null && targetExtension == null || sourceExtension == targetExtension); if (nop && delete == false) return; - string logName, baseFilename, baseLogFilename, mdfFilename, ldfFilename; GetDatabaseFiles(databaseName, filesPath, - out logName, out baseFilename, out baseLogFilename, out mdfFilename, out ldfFilename); + out _, out _, out _, out var mdfFilename, out var ldfFilename); if (sourceExtension != null) { @@ -809,9 +808,8 @@ namespace Umbraco.Core.Persistence else { // copy or copy+delete ie move - string targetLogName, targetBaseFilename, targetLogFilename, targetMdfFilename, targetLdfFilename; GetDatabaseFiles(targetDatabaseName ?? databaseName, targetFilesPath ?? filesPath, - out targetLogName, out targetBaseFilename, out targetLogFilename, out targetMdfFilename, out targetLdfFilename); + out _, out _, out _, out var targetMdfFilename, out var targetLdfFilename); if (targetExtension != null) { @@ -846,9 +844,8 @@ namespace Umbraco.Core.Persistence /// public bool DatabaseFilesExist(string databaseName, string filesPath, string extension = null) { - string logName, baseFilename, baseLogFilename, mdfFilename, ldfFilename; GetDatabaseFiles(databaseName, filesPath, - out logName, out baseFilename, out baseLogFilename, out mdfFilename, out ldfFilename); + out _, out _, out _, out var mdfFilename, out var ldfFilename); if (extension != null) { @@ -897,16 +894,13 @@ namespace Umbraco.Core.Persistence /// private int ExecuteSqlLocalDb(string args, out string output, out string error) { - var programFiles = Environment.GetEnvironmentVariable("ProgramFiles"); - if (programFiles == null) + if (_exe == null) // should never happen - we should not execute if not available { output = string.Empty; error = "SqlLocalDB.exe not found"; return -1; } - var path = Path.Combine(programFiles, string.Format(@"Microsoft SQL Server\{0}0\Tools\Binn\SqlLocalDB.exe", _version)); - var p = new Process { StartInfo = @@ -914,7 +908,7 @@ namespace Umbraco.Core.Persistence UseShellExecute = false, RedirectStandardOutput = true, RedirectStandardError = true, - FileName = path, + FileName = _exe, Arguments = args, CreateNoWindow = true, WindowStyle = ProcessWindowStyle.Hidden diff --git a/src/Umbraco.Core/Persistence/PocoDataDataReader.cs b/src/Umbraco.Core/Persistence/PocoDataDataReader.cs index 5b3333ee33..7bfc31f66c 100644 --- a/src/Umbraco.Core/Persistence/PocoDataDataReader.cs +++ b/src/Umbraco.Core/Persistence/PocoDataDataReader.cs @@ -40,7 +40,12 @@ namespace Umbraco.Core.Persistence _tableDefinition = DefinitionFactory.GetTableDefinition(pd.Type, sqlSyntaxProvider); if (_tableDefinition == null) throw new InvalidOperationException("No table definition found for type " + pd.Type); - _readerColumns = pd.Columns.Select(x => x.Value).ToArray(); + // only real columns, exclude result columns + _readerColumns = pd.Columns + .Where(x => x.Value.ResultColumn == false) + .Select(x => x.Value) + .ToArray(); + _sqlSyntaxProvider = sqlSyntaxProvider; _enumerator = dataSource.GetEnumerator(); _columnDefinitions = _tableDefinition.Columns.ToArray(); diff --git a/src/Umbraco.Core/Persistence/Repositories/IAuditEntryRepository.cs b/src/Umbraco.Core/Persistence/Repositories/IAuditEntryRepository.cs new file mode 100644 index 0000000000..1d4d2fe531 --- /dev/null +++ b/src/Umbraco.Core/Persistence/Repositories/IAuditEntryRepository.cs @@ -0,0 +1,22 @@ +using System.Collections.Generic; +using Umbraco.Core.Models; + +namespace Umbraco.Core.Persistence.Repositories +{ + /// + /// Represents a repository for entities. + /// + public interface IAuditEntryRepository : IReadWriteQueryRepository + { + /// + /// Gets a page of entries. + /// + IEnumerable GetPage(long pageIndex, int pageCount, out long records); + + /// + /// Determines whether the repository is available. + /// + /// During an upgrade, the repository may not be available, until the table has been created. + bool IsAvailable(); + } +} diff --git a/src/Umbraco.Core/Persistence/Repositories/IAuditRepository.cs b/src/Umbraco.Core/Persistence/Repositories/IAuditRepository.cs index f05dcfca91..7c8a82bb85 100644 --- a/src/Umbraco.Core/Persistence/Repositories/IAuditRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/IAuditRepository.cs @@ -1,9 +1,37 @@ -using Umbraco.Core.Models; +using System.Collections.Generic; +using Umbraco.Core.Models; +using Umbraco.Core.Persistence.DatabaseModelDefinitions; +using Umbraco.Core.Persistence.Querying; namespace Umbraco.Core.Persistence.Repositories { - public interface IAuditRepository : IReadRepository, IWriteRepository, IQueryRepository + public interface IAuditRepository : IReadRepository, IWriteRepository, IQueryRepository { void CleanLogs(int maximumAgeOfLogsInMinutes); + + /// + /// Return the audit items as paged result + /// + /// + /// The query coming from the service + /// + /// + /// + /// + /// + /// + /// Since we currently do not have enum support with our expression parser, we cannot query on AuditType in the query or the custom filter + /// so we need to do that here + /// + /// + /// A user supplied custom filter + /// + /// + IEnumerable GetPagedResultsByQuery( + IQuery query, + long pageIndex, int pageSize, out long totalRecords, + Direction orderDirection, + AuditType[] auditTypeFilter, + IQuery customFilter); } } diff --git a/src/Umbraco.Core/Persistence/Repositories/IConsentRepository.cs b/src/Umbraco.Core/Persistence/Repositories/IConsentRepository.cs new file mode 100644 index 0000000000..85cfb52ba3 --- /dev/null +++ b/src/Umbraco.Core/Persistence/Repositories/IConsentRepository.cs @@ -0,0 +1,15 @@ +using Umbraco.Core.Models; + +namespace Umbraco.Core.Persistence.Repositories +{ + /// + /// Represents a repository for entities. + /// + public interface IConsentRepository : IReadWriteQueryRepository + { + /// + /// Clears the current flag. + /// + void ClearCurrent(string source, string context, string action); + } +} diff --git a/src/Umbraco.Core/Persistence/Repositories/IMemberGroupRepository.cs b/src/Umbraco.Core/Persistence/Repositories/IMemberGroupRepository.cs index 7b3414e3ef..9c75c051bd 100644 --- a/src/Umbraco.Core/Persistence/Repositories/IMemberGroupRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/IMemberGroupRepository.cs @@ -39,5 +39,7 @@ namespace Umbraco.Core.Persistence.Repositories void AssignRoles(int[] memberIds, string[] roleNames); void DissociateRoles(int[] memberIds, string[] roleNames); + + int[] GetMemberIds(string[] names); } } diff --git a/src/Umbraco.Core/Persistence/Repositories/IUserRepository.cs b/src/Umbraco.Core/Persistence/Repositories/IUserRepository.cs index 0079f0c3e1..c9ed1af558 100644 --- a/src/Umbraco.Core/Persistence/Repositories/IUserRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/IUserRepository.cs @@ -85,5 +85,11 @@ namespace Umbraco.Core.Persistence.Repositories IProfile GetProfile(string username); IProfile GetProfile(int id); IDictionary GetUserStates(); + + Guid CreateLoginSession(int userId, string requestingIpAddress, bool cleanStaleSessions = true); + bool ValidateLoginSession(int userId, Guid sessionId); + int ClearLoginSessions(int userId); + int ClearLoginSessions(TimeSpan timespan); + void ClearLoginSession(Guid sessionId); } } diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/AuditEntryRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/AuditEntryRepository.cs new file mode 100644 index 0000000000..77759ea2da --- /dev/null +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/AuditEntryRepository.cs @@ -0,0 +1,137 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using NPoco; +using Umbraco.Core.Cache; +using Umbraco.Core.Logging; +using Umbraco.Core.Models; +using Umbraco.Core.Models.Entities; +using Umbraco.Core.Persistence.Dtos; +using Umbraco.Core.Persistence.Factories; +using Umbraco.Core.Persistence.Querying; +using Umbraco.Core.Scoping; + +namespace Umbraco.Core.Persistence.Repositories.Implement +{ + /// + /// Represents the NPoco implementation of . + /// + internal class AuditEntryRepository : NPocoRepositoryBase, IAuditEntryRepository + { + /// + /// Initializes a new instance of the class. + /// + public AuditEntryRepository(IScopeAccessor scopeAccessor, CacheHelper cache, ILogger logger) + : base(scopeAccessor, cache, logger) + { } + + /// + protected override Guid NodeObjectTypeId => throw new NotSupportedException(); + + /// + protected override IAuditEntry PerformGet(int id) + { + var sql = Sql() + .Select() + .From() + .Where(x => x.Id == id); + + var dto = Database.FirstOrDefault(sql); + return dto == null ? null : AuditEntryFactory.BuildEntity(dto); + } + + /// + protected override IEnumerable PerformGetAll(params int[] ids) + { + if (ids.Length == 0) + { + var sql = Sql() + .Select() + .From(); + + return Database.Fetch(sql).Select(AuditEntryFactory.BuildEntity); + } + + var entries = new List(); + + foreach (var group in ids.InGroupsOf(2000)) + { + var sql = Sql() + .Select() + .From() + .WhereIn(x => x.Id, group); + + entries.AddRange(Database.Fetch(sql).Select(AuditEntryFactory.BuildEntity)); + } + + return entries; + } + + /// + protected override IEnumerable PerformGetByQuery(IQuery query) + { + var sqlClause = GetBaseQuery(false); + var translator = new SqlTranslator(sqlClause, query); + var sql = translator.Translate(); + return Database.Fetch(sql).Select(AuditEntryFactory.BuildEntity); + } + + /// + protected override Sql GetBaseQuery(bool isCount) + { + var sql = Sql(); + sql = isCount ? sql.SelectCount() : sql.Select(); + sql = sql.From(); + return sql; + } + + /// + protected override string GetBaseWhereClause() + { + return $"{Constants.DatabaseSchema.Tables.AuditEntry}.id = @id"; + } + + /// + protected override IEnumerable GetDeleteClauses() + { + throw new NotSupportedException("Audit entries cannot be deleted."); + } + + /// + protected override void PersistNewItem(IAuditEntry entity) + { + ((EntityBase) entity).AddingEntity(); + + var dto = AuditEntryFactory.BuildDto(entity); + Database.Insert(dto); + entity.Id = dto.Id; + entity.ResetDirtyProperties(); + } + + /// + protected override void PersistUpdatedItem(IAuditEntry entity) + { + throw new NotSupportedException("Audit entries cannot be updated."); + } + + /// + public IEnumerable GetPage(long pageIndex, int pageCount, out long records) + { + var sql = Sql() + .Select() + .From() + .OrderByDescending(x => x.EventDateUtc); + + var page = Database.Page(pageIndex + 1, pageCount, sql); + records = page.TotalItems; + return page.Items.Select(AuditEntryFactory.BuildEntity); + } + + /// + public bool IsAvailable() + { + var tables = SqlSyntax.GetTablesInSchema(Database).ToArray(); + return tables.InvariantContains(Constants.DatabaseSchema.Tables.AuditEntry); + } + } +} diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/AuditRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/AuditRepository.cs index 26f02705e7..fd7161e48f 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/AuditRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/AuditRepository.cs @@ -7,19 +7,20 @@ using Umbraco.Core.Cache; using Umbraco.Core.Composing.CompositionRoots; using Umbraco.Core.Logging; using Umbraco.Core.Models; +using Umbraco.Core.Persistence.DatabaseModelDefinitions; using Umbraco.Core.Persistence.Dtos; using Umbraco.Core.Persistence.Querying; using Umbraco.Core.Scoping; namespace Umbraco.Core.Persistence.Repositories.Implement { - internal class AuditRepository : NPocoRepositoryBase, IAuditRepository + internal class AuditRepository : NPocoRepositoryBase, IAuditRepository { public AuditRepository(IScopeAccessor scopeAccessor, [Inject(RepositoryCompositionRoot.DisabledCache)] CacheHelper cache, ILogger logger) : base(scopeAccessor, cache, logger) { } - protected override void PersistNewItem(AuditItem entity) + protected override void PersistNewItem(IAuditItem entity) { Database.Insert(new LogDto { @@ -31,8 +32,9 @@ namespace Umbraco.Core.Persistence.Repositories.Implement }); } - protected override void PersistUpdatedItem(AuditItem entity) + protected override void PersistUpdatedItem(IAuditItem entity) { + // wtf?! inserting when updating?! Database.Insert(new LogDto { Comment = entity.Comment, @@ -43,27 +45,26 @@ namespace Umbraco.Core.Persistence.Repositories.Implement }); } - protected override AuditItem PerformGet(int id) + protected override IAuditItem PerformGet(int id) { var sql = GetBaseQuery(false); sql.Where(GetBaseWhereClause(), new { Id = id }); var dto = Database.First(sql); - if (dto == null) - return null; - - return new AuditItem(dto.NodeId, dto.Comment, Enum.Parse(dto.Header), dto.UserId); + return dto == null + ? null + : new AuditItem(dto.NodeId, dto.Comment, Enum.Parse(dto.Header), dto.UserId); } - protected override IEnumerable PerformGetAll(params int[] ids) + protected override IEnumerable PerformGetAll(params int[] ids) { throw new NotImplementedException(); } - protected override IEnumerable PerformGetByQuery(IQuery query) + protected override IEnumerable PerformGetByQuery(IQuery query) { var sqlClause = GetBaseQuery(false); - var translator = new SqlTranslator(sqlClause, query); + var translator = new SqlTranslator(sqlClause, query); var sql = translator.Translate(); var dtos = Database.Fetch(sql); @@ -82,6 +83,9 @@ namespace Umbraco.Core.Persistence.Repositories.Implement sql .From(); + if (!isCount) + sql.LeftJoin().On((left, right) => left.UserId == right.Id); + return sql; } @@ -108,5 +112,61 @@ namespace Umbraco.Core.Persistence.Repositories.Implement "delete from umbracoLog where datestamp < @oldestPermittedLogEntry and logHeader in ('open','system')", new {oldestPermittedLogEntry = oldestPermittedLogEntry}); } + + /// + /// Return the audit items as paged result + /// + /// + /// The query coming from the service + /// + /// + /// + /// + /// + /// + /// Since we currently do not have enum support with our expression parser, we cannot query on AuditType in the query or the custom filter + /// so we need to do that here + /// + /// + /// A user supplied custom filter + /// + /// + public IEnumerable GetPagedResultsByQuery(IQuery query, long pageIndex, int pageSize, + out long totalRecords, Direction orderDirection, + AuditType[] auditTypeFilter, + IQuery customFilter) + { + if (auditTypeFilter == null) auditTypeFilter = Array.Empty(); + + var sql = GetBaseQuery(false); + + var translator = new SqlTranslator(sql, query ?? Query()); + sql = translator.Translate(); + + if (customFilter != null) + foreach (var filterClause in customFilter.GetWhereClauses()) + sql.Where(filterClause.Item1, filterClause.Item2); + + if (auditTypeFilter.Length > 0) + foreach (var type in auditTypeFilter) + sql.Where("(logHeader=@0)", type.ToString()); + + sql = orderDirection == Direction.Ascending + ? sql.OrderBy("Datestamp") + : sql.OrderByDescending("Datestamp"); + + // get page + var page = Database.Page(pageIndex + 1, pageSize, sql); + totalRecords = page.TotalItems; + + var items = page.Items.Select( + dto => new AuditItem(dto.Id, dto.Comment, Enum.Parse(dto.Header), dto.UserId)).ToArray(); + + // map the DateStamp + for (var i = 0; i < items.Length; i++) + items[i].CreateDate = page.Items[i].Datestamp; + + return items; + } } } diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/ConsentRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/ConsentRepository.cs new file mode 100644 index 0000000000..3794bf183a --- /dev/null +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/ConsentRepository.cs @@ -0,0 +1,101 @@ +using System; +using System.Collections.Generic; +using NPoco; +using Umbraco.Core.Cache; +using Umbraco.Core.Logging; +using Umbraco.Core.Models; +using Umbraco.Core.Models.Entities; +using Umbraco.Core.Persistence.Querying; +using Umbraco.Core.Persistence.Dtos; +using Umbraco.Core.Persistence.Factories; +using Umbraco.Core.Scoping; + +namespace Umbraco.Core.Persistence.Repositories.Implement +{ + /// + /// Represents the NPoco implementation of . + /// + internal class ConsentRepository : NPocoRepositoryBase, IConsentRepository + { + /// + /// Initializes a new instance of the class. + /// + public ConsentRepository(IScopeAccessor scopeAccessor, CacheHelper cache, ILogger logger) + : base(scopeAccessor, cache, logger) + { } + + /// + protected override Guid NodeObjectTypeId => throw new NotSupportedException(); + + /// + protected override IConsent PerformGet(int id) + { + throw new NotSupportedException(); + } + + /// + protected override IEnumerable PerformGetAll(params int[] ids) + { + throw new NotSupportedException(); + } + + /// + protected override IEnumerable PerformGetByQuery(IQuery query) + { + var sqlClause = Sql().Select().From(); + var translator = new SqlTranslator(sqlClause, query); + var sql = translator.Translate().OrderByDescending(x => x.CreateDate); + return ConsentFactory.BuildEntities(Database.Fetch(sql)); + } + + /// + protected override Sql GetBaseQuery(bool isCount) + { + throw new NotSupportedException(); + } + + /// + protected override string GetBaseWhereClause() + { + throw new NotSupportedException(); + } + + /// + protected override IEnumerable GetDeleteClauses() + { + throw new NotSupportedException(); + } + + /// + protected override void PersistNewItem(IConsent entity) + { + ((EntityBase) entity).AddingEntity(); + + var dto = ConsentFactory.BuildDto(entity); + Database.Insert(dto); + entity.Id = dto.Id; + entity.ResetDirtyProperties(); + } + + /// + protected override void PersistUpdatedItem(IConsent entity) + { + ((EntityBase) entity).UpdatingEntity(); + + var dto = ConsentFactory.BuildDto(entity); + Database.Update(dto); + entity.ResetDirtyProperties(); + + IsolatedCache.ClearCacheItem(RepositoryCacheKeys.GetKey(entity.Id)); + } + + /// + public void ClearCurrent(string source, string context, string action) + { + var sql = Sql() + .Update(u => u.Set(x => x.Current, false)) + .Where(x => x.Source == source && x.Context == context && x.Action == action && x.Current); + Database.Execute(sql); + } + } +} diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/MediaRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/MediaRepository.cs index 52d137cb16..a08ecef98d 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/MediaRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/MediaRepository.cs @@ -80,10 +80,10 @@ namespace Umbraco.Core.Persistence.Repositories.Implement protected override Sql GetBaseQuery(QueryType queryType) { - return GetBaseQuery(queryType, true); + return GetBaseQuery(queryType); } - protected virtual Sql GetBaseQuery(QueryType queryType, bool current) + protected virtual Sql GetBaseQuery(QueryType queryType, bool current = true, bool joinMediaVersion = false) { var sql = SqlContext.Sql(); @@ -108,6 +108,8 @@ namespace Umbraco.Core.Persistence.Repositories.Implement .InnerJoin().On(left => left.NodeId, right => right.NodeId) .InnerJoin().On(left => left.NodeId, right => right.NodeId); + if (joinMediaVersion) + sql.InnerJoin().On((left, right) => left.Id == right.Id); sql.Where(x => x.NodeObjectType == NodeObjectTypeId); @@ -143,6 +145,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement "DELETE FROM " + Constants.DatabaseSchema.Tables.Relation + " WHERE childId = @id", "DELETE FROM " + Constants.DatabaseSchema.Tables.TagRelationship + " WHERE nodeId = @id", "DELETE FROM " + Constants.DatabaseSchema.Tables.Document + " WHERE nodeId = @id", + "DELETE FROM " + Constants.DatabaseSchema.Tables.MediaVersion + " WHERE id IN (SELECT id FROM " + Constants.DatabaseSchema.Tables.ContentVersion + " WHERE nodeId = @id)", "DELETE FROM " + Constants.DatabaseSchema.Tables.PropertyData + " WHERE versionId IN (SELECT id FROM " + Constants.DatabaseSchema.Tables.ContentVersion + " WHERE nodeId = @id)", "DELETE FROM " + Constants.DatabaseSchema.Tables.ContentVersion + " WHERE nodeId = @id", "DELETE FROM " + Constants.DatabaseSchema.Tables.Content + " WHERE nodeId = @id", @@ -157,7 +160,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement public override IEnumerable GetAllVersions(int nodeId) { - var sql = GetBaseQuery(QueryType.Many, false) + var sql = GetBaseQuery(QueryType.Many, current: false) .Where(x => x.NodeId == nodeId) .OrderByDescending(x => x.Current) .AndByDescending(x => x.VersionDate); @@ -188,29 +191,14 @@ namespace Umbraco.Core.Persistence.Repositories.Implement umbracoFileValue = string.Concat(mediaPath.Substring(0, underscoreIndex), mediaPath.Substring(dotIndex)); } - // If the stripped-down url returns null, we try again with the original url. - // Previously, the function would fail on e.g. "my_x_image.jpg" - var nodeId = GetMediaNodeIdByPath(Sql().Where(x => x.VarcharValue == umbracoFileValue)); - if (nodeId < 0) nodeId = GetMediaNodeIdByPath(Sql().Where(x => x.VarcharValue == mediaPath)); + var sql = GetBaseQuery(QueryType.Single, joinMediaVersion: true) + .Where(x => x.Path == umbracoFileValue) + .SelectTop(1); - // If no result so far, try getting from a json value stored in the ntext / nvarchar column - if (nodeId < 0) nodeId = GetMediaNodeIdByPath(Sql().Where("textValue LIKE @0", "%" + umbracoFileValue + "%")); - if (nodeId < 0) nodeId = GetMediaNodeIdByPath(Sql().Where("varcharValue LIKE @0", "%" + umbracoFileValue + "%")); - - return nodeId < 0 ? null : Get(nodeId); - } - - private int GetMediaNodeIdByPath(Sql query) - { - var sql = Sql().Select(x => x.NodeId) - .From() - .InnerJoin().On(left => left.PropertyTypeId, right => right.Id) - .InnerJoin().On((left, right) => left.VersionId == right.Id) - .Where(x => x.Alias == "umbracoFile") - .Append(query); - - var nodeId = Database.Fetch(sql).FirstOrDefault(); - return nodeId ?? -1; + var dto = Database.Fetch(sql).FirstOrDefault(); + return dto == null + ? null + : MapDtoToContent(dto); } protected override void PerformDeleteVersion(int id, int versionId) @@ -249,7 +237,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement var sortOrder = GetNewChildSortOrder(entity.ParentId, 0); // persist the node dto - var nodeDto = dto.NodeDto; + var nodeDto = dto.ContentDto.NodeDto; nodeDto.Path = parent.Path; nodeDto.Level = Convert.ToInt16(level); nodeDto.SortOrder = sortOrder; @@ -258,21 +246,13 @@ namespace Umbraco.Core.Persistence.Repositories.Implement // and then either update or insert the node dto var id = GetReservedId(nodeDto.UniqueId); if (id > 0) - { nodeDto.NodeId = id; - nodeDto.Path = string.Concat(parent.Path, ",", nodeDto.NodeId); - nodeDto.ValidatePathWithException(); - Database.Update(nodeDto); - } else - { Database.Insert(nodeDto); - // update path, now that we have an id - nodeDto.Path = string.Concat(parent.Path, ",", nodeDto.NodeId); - nodeDto.ValidatePathWithException(); - Database.Update(nodeDto); - } + nodeDto.Path = string.Concat(parent.Path, ",", nodeDto.NodeId); + nodeDto.ValidatePathWithException(); + Database.Update(nodeDto); // update entity entity.Id = nodeDto.NodeId; @@ -281,17 +261,23 @@ namespace Umbraco.Core.Persistence.Repositories.Implement entity.Level = level; // persist the content dto - dto.NodeId = nodeDto.NodeId; - Database.Insert(dto); + var contentDto = dto.ContentDto; + contentDto.NodeId = nodeDto.NodeId; + Database.Insert(contentDto); // persist the content version dto // assumes a new version id and version date (modified date) has been set - var contentVersionDto = dto.ContentVersionDto; + var contentVersionDto = dto.MediaVersionDto.ContentVersionDto; contentVersionDto.NodeId = nodeDto.NodeId; contentVersionDto.Current = true; Database.Insert(contentVersionDto); media.VersionId = contentVersionDto.Id; + // persist the media version dto + var mediaVersionDto = dto.MediaVersionDto; + mediaVersionDto.Id = media.VersionId; + Database.Insert(mediaVersionDto); + // persist the property data var propertyDataDtos = PropertyFactory.BuildDtos(media.VersionId, 0, entity.Properties, out _); foreach (var propertyDataDto in propertyDataDtos) @@ -333,17 +319,19 @@ namespace Umbraco.Core.Persistence.Repositories.Implement var dto = ContentBaseFactory.BuildDto(entity); // update the node dto - var nodeDto = dto.NodeDto; + var nodeDto = dto.ContentDto.NodeDto; nodeDto.ValidatePathWithException(); Database.Update(nodeDto); // update the content dto - Database.Update(dto); + Database.Update(dto.ContentDto); - // update the content version dto - var contentVersionDto = dto.ContentVersionDto; + // 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().Where(x => x.VersionId == media.VersionId); diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/MemberGroupRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/MemberGroupRepository.cs index 0e7c18597b..f29fef1102 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/MemberGroupRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/MemberGroupRepository.cs @@ -173,20 +173,19 @@ namespace Umbraco.Core.Persistence.Repositories.Implement .Select("un.*") .From("umbracoNode AS un") .InnerJoin("cmsMember2MemberGroup") - .On("un.id = cmsMember2MemberGroup.MemberGroup") - .LeftJoin("(SELECT umbracoNode.id, cmsMember.LoginName FROM umbracoNode INNER JOIN cmsMember ON umbracoNode.id = cmsMember.nodeId) AS member") - .On("member.id = cmsMember2MemberGroup.Member") - .Where("un.nodeObjectType=@objectType", new {objectType = NodeObjectTypeId }) - .Where("member.LoginName=@loginName", new {loginName = username}); + .On("cmsMember2MemberGroup.MemberGroup = un.id") + .InnerJoin("cmsMember") + .On("cmsMember.nodeId = cmsMember2MemberGroup.Member") + .Where("un.nodeObjectType=@objectType", new { objectType = NodeObjectTypeId }) + .Where("cmsMember.LoginName=@loginName", new { loginName = username }); return Database.Fetch(sql) .DistinctBy(dto => dto.NodeId) .Select(x => _modelFactory.BuildEntity(x)); } - public void AssignRoles(string[] usernames, string[] roleNames) + public int[] GetMemberIds(string[] usernames) { - //first get the member ids based on the usernames var memberObjectType = Constants.ObjectTypes.Member; var memberSql = Sql() @@ -196,26 +195,17 @@ namespace Umbraco.Core.Persistence.Repositories.Implement .On(dto => dto.NodeId, dto => dto.NodeId) .Where(x => x.NodeObjectType == memberObjectType) .Where("cmsMember.LoginName in (@usernames)", new { /*usernames =*/ usernames }); - var memberIds = Database.Fetch(memberSql).ToArray(); + return Database.Fetch(memberSql).ToArray(); + } - AssignRolesInternal(memberIds, roleNames); + public void AssignRoles(string[] usernames, string[] roleNames) + { + AssignRolesInternal(GetMemberIds(usernames), roleNames); } public void DissociateRoles(string[] usernames, string[] roleNames) { - //first get the member ids based on the usernames - var memberObjectType = Constants.ObjectTypes.Member; - - var memberSql = Sql() - .Select("umbracoNode.id") - .From() - .InnerJoin() - .On(dto => dto.NodeId, dto => dto.NodeId) - .Where( x => x.NodeObjectType == memberObjectType) - .Where("cmsMember.LoginName in (@usernames)", new { /*usernames =*/ usernames }); - var memberIds = Database.Fetch(memberSql).ToArray(); - - DissociateRolesInternal(memberIds, roleNames); + DissociateRolesInternal(GetMemberIds(usernames), roleNames); } public void AssignRoles(int[] memberIds, string[] roleNames) diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/MemberTypeRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/MemberTypeRepository.cs index 6fef47679b..72f59c0d2a 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/MemberTypeRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/MemberTypeRepository.cs @@ -147,7 +147,8 @@ namespace Umbraco.Core.Persistence.Repositories.Implement .Select("umbracoNode.*", "cmsContentType.*", "cmsPropertyType.id AS PropertyTypeId", "cmsPropertyType.Alias", "cmsPropertyType.Name", "cmsPropertyType.Description", "cmsPropertyType.mandatory", "cmsPropertyType.UniqueID", "cmsPropertyType.validationRegExp", "cmsPropertyType.dataTypeId", "cmsPropertyType.sortOrder AS PropertyTypeSortOrder", - "cmsPropertyType.propertyTypeGroupId AS PropertyTypesGroupId", "cmsMemberType.memberCanEdit", "cmsMemberType.viewOnProfile", + "cmsPropertyType.propertyTypeGroupId AS PropertyTypesGroupId", + "cmsMemberType.memberCanEdit", "cmsMemberType.viewOnProfile", "cmsMemberType.isSensitive", "uDataType.propertyEditorAlias", "uDataType.dbType", "cmsPropertyTypeGroup.id AS PropertyTypeGroupId", "cmsPropertyTypeGroup.text AS PropertyGroupName", "cmsPropertyTypeGroup.uniqueID AS PropertyGroupUniqueID", "cmsPropertyTypeGroup.sortorder AS PropertyGroupSortOrder", "cmsPropertyTypeGroup.contenttypeNodeId") diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/SimilarNodeName.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/SimilarNodeName.cs index 56a520902c..9f27b6b9e3 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/SimilarNodeName.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/SimilarNodeName.cs @@ -20,6 +20,10 @@ namespace Umbraco.Core.Persistence.Repositories.Implement var name = Name; + // cater nodes with no name. + if (string.IsNullOrWhiteSpace(name)) + return _numPos; + if (name[name.Length - 1] != ')') return _numPos = -1; @@ -106,7 +110,9 @@ namespace Umbraco.Core.Persistence.Repositories.Implement } } - return uniqueing ? string.Concat(nodeName, " (", uniqueNumber.ToString(), ")") : nodeName; + return uniqueing || string.IsNullOrWhiteSpace(nodeName) + ? string.Concat(nodeName, " (", uniqueNumber.ToString(), ")") + : nodeName; } } } diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/UserGroupRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/UserGroupRepository.cs index 9f9cae3288..91466a0d09 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/UserGroupRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/UserGroupRepository.cs @@ -298,6 +298,8 @@ namespace Umbraco.Core.Persistence.Repositories.Implement entity.Id = id; PersistAllowedSections(entity); + + entity.ResetDirtyProperties(); } protected override void PersistUpdatedItem(IUserGroup entity) @@ -309,6 +311,8 @@ namespace Umbraco.Core.Persistence.Repositories.Implement Database.Update(userGroupDto); PersistAllowedSections(entity); + + entity.ResetDirtyProperties(); } private void PersistAllowedSections(IUserGroup entity) diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/UserRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/UserRepository.cs index 9ce7358408..636c9fda69 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/UserRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/UserRepository.cs @@ -7,6 +7,7 @@ using System.Web.Security; using Newtonsoft.Json; using NPoco; using Umbraco.Core.Cache; +using Umbraco.Core.Configuration; using Umbraco.Core.Logging; using Umbraco.Core.Models.Entities; using Umbraco.Core.Models.Membership; @@ -151,14 +152,91 @@ ORDER BY colName"; return new Dictionary { - {UserState.All, result[0].num}, - {UserState.Active, result[1].num}, - {UserState.Disabled, result[2].num}, - {UserState.LockedOut, result[3].num}, - {UserState.Invited, result[4].num} + {UserState.All, (int) result[0].num}, + {UserState.Active, (int) result[1].num}, + {UserState.Disabled, (int) result[2].num}, + {UserState.LockedOut, (int) result[3].num}, + {UserState.Invited, (int) result[4].num} }; } + public Guid CreateLoginSession(int userId, string requestingIpAddress, bool cleanStaleSessions = true) + { + //TODO: I know this doesn't follow the normal repository conventions which would require us to crete a UserSessionRepository + //and also business logic models for these objects but that's just so overkill for what we are doing + //and now that everything is properly in a transaction (Scope) there doesn't seem to be much reason for using that anymore + var now = DateTime.UtcNow; + var dto = new UserLoginDto + { + UserId = userId, + IpAddress = requestingIpAddress, + LoggedInUtc = now, + LastValidatedUtc = now, + LoggedOutUtc = null, + SessionId = Guid.NewGuid() + }; + Database.Insert(dto); + + if (cleanStaleSessions) + { + ClearLoginSessions(TimeSpan.FromDays(15)); + } + + return dto.SessionId; + } + + public bool ValidateLoginSession(int userId, Guid sessionId) + { + var found = Database.FirstOrDefault("WHERE sessionId=@sessionId", new {sessionId = sessionId}); + if (found == null || found.UserId != userId || found.LoggedOutUtc.HasValue) + return false; + + //now detect if there's been a timeout + if (DateTime.UtcNow - found.LastValidatedUtc > TimeSpan.FromMinutes(GlobalSettings.TimeOutInMinutes)) + { + //timeout detected, update the record + ClearLoginSession(sessionId); + return false; + } + + //update the validate date + found.LastValidatedUtc = DateTime.UtcNow; + Database.Update(found); + return true; + } + + public int ClearLoginSessions(int userId) + { + //TODO: I know this doesn't follow the normal repository conventions which would require us to crete a UserSessionRepository + //and also business logic models for these objects but that's just so overkill for what we are doing + //and now that everything is properly in a transaction (Scope) there doesn't seem to be much reason for using that anymore + var count = Database.ExecuteScalar("SELECT COUNT(*) FROM umbracoUserLogin WHERE userId=@userId", new { userId = userId }); + Database.Execute("DELETE FROM umbracoUserLogin WHERE userId=@userId", new {userId = userId}); + return count; + } + + public int ClearLoginSessions(TimeSpan timespan) + { + //TODO: I know this doesn't follow the normal repository conventions which would require us to crete a UserSessionRepository + //and also business logic models for these objects but that's just so overkill for what we are doing + //and now that everything is properly in a transaction (Scope) there doesn't seem to be much reason for using that anymore + + var fromDate = DateTime.UtcNow - timespan; + + var count = Database.ExecuteScalar("SELECT COUNT(*) FROM umbracoUserLogin WHERE lastValidatedUtc=@fromDate", new { fromDate = fromDate }); + Database.Execute("DELETE FROM umbracoUserLogin WHERE lastValidatedUtc=@fromDate", new { fromDate = fromDate }); + return count; + } + + public void ClearLoginSession(Guid sessionId) + { + //TODO: I know this doesn't follow the normal repository conventions which would require us to crete a UserSessionRepository + //and also business logic models for these objects but that's just so overkill for what we are doing + //and now that everything is properly in a transaction (Scope) there doesn't seem to be much reason for using that anymore + Database.Execute("UPDATE umbracoUserLogin SET loggedOutUtc=@now WHERE sessionId=@sessionId", + new { now = DateTime.UtcNow, sessionId = sessionId }); + } + protected override IEnumerable PerformGetAll(params int[] ids) { var dtos = ids.Length == 0 @@ -429,7 +507,8 @@ ORDER BY colName"; {"updateDate", "UpdateDate"}, {"avatar", "Avatar"}, {"emailConfirmedDate", "EmailConfirmedDate"}, - {"invitedDate", "InvitedDate"} + {"invitedDate", "InvitedDate"}, + {"tourData", "TourData"} }; // create list of properties that have changed diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index a2854ea1be..9719a5f223 100644 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -373,6 +373,10 @@ + + + +