diff --git a/src/Umbraco.Core/Services/UserServiceExtensions.cs b/src/Umbraco.Core/Services/UserServiceExtensions.cs index 480222b351..4f5eb3c948 100644 --- a/src/Umbraco.Core/Services/UserServiceExtensions.cs +++ b/src/Umbraco.Core/Services/UserServiceExtensions.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Linq; using Umbraco.Core.Models.Membership; @@ -68,5 +69,17 @@ namespace Umbraco.Core.Services userService.ReplaceUserGroupPermissions(groupId, new char[] { }); } + + public static IEnumerable GetProfilesById(this IUserService userService, params int[] ids) + { + var fullUsers = userService.GetUsersById(ids); + + return fullUsers.Select(user => + { + var asProfile = user as IProfile; + return asProfile ?? new UserProfile(user.Id, user.Name); + }); + + } } } diff --git a/src/Umbraco.Infrastructure/Examine/ContentValueSetBuilder.cs b/src/Umbraco.Infrastructure/Examine/ContentValueSetBuilder.cs index c115939b7d..01e6d993b4 100644 --- a/src/Umbraco.Infrastructure/Examine/ContentValueSetBuilder.cs +++ b/src/Umbraco.Infrastructure/Examine/ContentValueSetBuilder.cs @@ -3,7 +3,9 @@ using System.Collections.Generic; using System.Linq; using Umbraco.Core; using Umbraco.Core.Models; +using Umbraco.Core.Models.Membership; using Umbraco.Core.PropertyEditors; +using Umbraco.Core.Scoping; using Umbraco.Core.Services; using Umbraco.Core.Strings; @@ -16,23 +18,41 @@ namespace Umbraco.Examine { private readonly UrlSegmentProviderCollection _urlSegmentProviders; private readonly IUserService _userService; + private readonly IScopeProvider _scopeProvider; + private readonly IShortStringHelper _shortStringHelper; public ContentValueSetBuilder(PropertyEditorCollection propertyEditors, UrlSegmentProviderCollection urlSegmentProviders, IUserService userService, IShortStringHelper shortStringHelper, + IScopeProvider scopeProvider, bool publishedValuesOnly) : base(propertyEditors, publishedValuesOnly) { _urlSegmentProviders = urlSegmentProviders; _userService = userService; _shortStringHelper = shortStringHelper; + _scopeProvider = scopeProvider; } /// public override IEnumerable GetValueSets(params IContent[] content) { + Dictionary creatorIds; + Dictionary writerIds; + + // We can lookup all of the creator/writer names at once which can save some + // processing below instead of one by one. + using (var scope = _scopeProvider.CreateScope()) + { + creatorIds = _userService.GetProfilesById(content.Select(x => x.CreatorId).ToArray()) + .ToDictionary(x => x.Id, x => x); + writerIds = _userService.GetProfilesById(content.Select(x => x.WriterId).ToArray()) + .ToDictionary(x => x.Id, x => x); + scope.Complete(); + } + // TODO: There is a lot of boxing going on here and ultimately all values will be boxed by Lucene anyways // but I wonder if there's a way to reduce the boxing that we have to do or if it will matter in the end since // Lucene will do it no matter what? One idea was to create a `FieldValue` struct which would contain `object`, `object[]`, `ValueType` and `ValueType[]` @@ -61,8 +81,8 @@ namespace Umbraco.Examine {"urlName", urlValue?.Yield() ?? Enumerable.Empty()}, //Always add invariant urlName {"path", c.Path?.Yield() ?? Enumerable.Empty()}, {"nodeType", c.ContentType.Id.ToString().Yield() ?? Enumerable.Empty()}, - {"creatorName", (c.GetCreatorProfile(_userService)?.Name ?? "??").Yield() }, - {"writerName",(c.GetWriterProfile(_userService)?.Name ?? "??").Yield() }, + {"creatorName", (creatorIds.TryGetValue(c.CreatorId, out var creatorProfile) ? creatorProfile.Name : "??").Yield() }, + {"writerName", (writerIds.TryGetValue(c.WriterId, out var writerProfile) ? writerProfile.Name : "??").Yield() }, {"writerID", new object[] {c.WriterId}}, {"templateID", new object[] {c.TemplateId ?? 0}}, {UmbracoExamineFieldNames.VariesByCultureFieldName, new object[] {"n"}}, diff --git a/src/Umbraco.Infrastructure/Logging/Viewer/LogViewerComposer.cs b/src/Umbraco.Infrastructure/Logging/Viewer/LogViewerComposer.cs index ee115be325..8244fa696d 100644 --- a/src/Umbraco.Infrastructure/Logging/Viewer/LogViewerComposer.cs +++ b/src/Umbraco.Infrastructure/Logging/Viewer/LogViewerComposer.cs @@ -1,5 +1,7 @@ -using Umbraco.Core.Composing; -using Umbraco.Core.IO; +using System.IO; +using Serilog; +using Umbraco.Core.Composing; +using Umbraco.Core.Hosting; namespace Umbraco.Core.Logging.Viewer { @@ -9,8 +11,26 @@ namespace Umbraco.Core.Logging.Viewer { public void Compose(Composition composition) { + + + composition.RegisterUnique(factory => + { + var hostingEnvironment = factory.GetInstance(); + return new LoggingConfiguration( + Path.Combine(hostingEnvironment.ApplicationPhysicalPath, "App_Data\\Logs"), + Path.Combine(hostingEnvironment.ApplicationPhysicalPath, "config\\serilog.config"), + Path.Combine(hostingEnvironment.ApplicationPhysicalPath, "config\\serilog.user.config")); + }); composition.RegisterUnique(); composition.SetLogViewer(); + composition.RegisterUnique(factory => + { + + return new SerilogJsonLogViewer(factory.GetInstance(), + factory.GetInstance(), + factory.GetInstance(), + Log.Logger); + } ); } } } diff --git a/src/Umbraco.Infrastructure/Logging/Viewer/SerilogLogViewerSourceBase.cs b/src/Umbraco.Infrastructure/Logging/Viewer/SerilogLogViewerSourceBase.cs index 7c8503a37e..278f3d8d00 100644 --- a/src/Umbraco.Infrastructure/Logging/Viewer/SerilogLogViewerSourceBase.cs +++ b/src/Umbraco.Infrastructure/Logging/Viewer/SerilogLogViewerSourceBase.cs @@ -14,7 +14,7 @@ namespace Umbraco.Core.Logging.Viewer private readonly global::Serilog.ILogger _serilogLog; protected SerilogLogViewerSourceBase(ILogViewerConfig logViewerConfig, global::Serilog.ILogger serilogLog) - { + { _logViewerConfig = logViewerConfig; _serilogLog = serilogLog; } @@ -50,7 +50,7 @@ namespace Umbraco.Core.Logging.Viewer /// public string GetLogLevel() { - var logLevel = Enum.GetValues(typeof(LogEventLevel)).Cast().Where(_serilogLog.IsEnabled)?.Min() ?? null; + var logLevel = Enum.GetValues(typeof(LogEventLevel)).Cast().Where(_serilogLog.IsEnabled).DefaultIfEmpty(LogEventLevel.Information)?.Min() ?? null; return logLevel?.ToString() ?? ""; } @@ -131,6 +131,6 @@ namespace Umbraco.Core.Logging.Viewer }; } - + } } diff --git a/src/Umbraco.Infrastructure/Persistence/NPocoDatabaseExtensions.cs b/src/Umbraco.Infrastructure/Persistence/NPocoDatabaseExtensions.cs index 8b4e92c368..6c9fff8798 100644 --- a/src/Umbraco.Infrastructure/Persistence/NPocoDatabaseExtensions.cs +++ b/src/Umbraco.Infrastructure/Persistence/NPocoDatabaseExtensions.cs @@ -15,6 +15,48 @@ namespace Umbraco.Core.Persistence /// public static partial class NPocoDatabaseExtensions { + /// + /// Iterates over the result of a paged data set with a db reader + /// + /// + /// + /// + /// The number of rows to load per page + /// + /// + /// + /// + /// NPoco's normal Page returns a List{T} but sometimes we don't want all that in memory and instead want to + /// iterate over each row with a reader using Query vs Fetch. + /// + public static IEnumerable QueryPaged(this IDatabase database, long pageSize, Sql sql) + { + var sqlString = sql.SQL; + var sqlArgs = sql.Arguments; + + int? itemCount = null; + long pageIndex = 0; + do + { + // Get the paged queries + database.BuildPageQueries(pageIndex * pageSize, pageSize, sqlString, ref sqlArgs, out var sqlCount, out var sqlPage); + + // get the item count once + if (itemCount == null) + { + itemCount = database.ExecuteScalar(sqlCount, sqlArgs); + } + pageIndex++; + + // iterate over rows without allocating all items to memory (Query vs Fetch) + foreach (var row in database.Query(sqlPage, sqlArgs)) + { + yield return row; + } + + } while ((pageIndex * pageSize) < itemCount); + } + // NOTE // // proper way to do it with TSQL and SQLCE diff --git a/src/Umbraco.Infrastructure/Runtime/SqlMainDomLock.cs b/src/Umbraco.Infrastructure/Runtime/SqlMainDomLock.cs index 85d915b433..a52fc652bb 100644 --- a/src/Umbraco.Infrastructure/Runtime/SqlMainDomLock.cs +++ b/src/Umbraco.Infrastructure/Runtime/SqlMainDomLock.cs @@ -43,6 +43,12 @@ namespace Umbraco.Core.Runtime public async Task AcquireLockAsync(int millisecondsTimeout) { + if (!_dbFactory.Configured) + { + // if we aren't configured, then we're in an install state, in which case we have no choice but to assume we can acquire + return true; + } + if (!(_dbFactory.SqlContext.SqlSyntax is SqlServerSyntaxProvider sqlServerSyntaxProvider)) throw new NotSupportedException("SqlMainDomLock is only supported for Sql Server"); @@ -129,6 +135,12 @@ namespace Umbraco.Core.Runtime // poll every 1 second Thread.Sleep(1000); + if (!_dbFactory.Configured) + { + // if we aren't configured, we just keep looping since we can't query the db + continue; + } + lock (_locker) { // If cancellation has been requested we will just exit. Depending on timing of the shutdown, @@ -361,41 +373,44 @@ namespace Umbraco.Core.Runtime _cancellationTokenSource.Cancel(); _cancellationTokenSource.Dispose(); - var db = GetDatabase(); - try + if (_dbFactory.Configured) { - db.BeginTransaction(IsolationLevel.ReadCommitted); - - // get a write lock - _sqlServerSyntax.WriteLock(db, Constants.Locks.MainDom); - - // When we are disposed, it means we have released the MainDom lock - // and called all MainDom release callbacks, in this case - // if another maindom is actually coming online we need - // to signal to the MainDom coming online that we have shutdown. - // To do that, we update the existing main dom DB record with a suffixed "_updated" string. - // Otherwise, if we are just shutting down, we want to just delete the row. - if (_mainDomChanging) + var db = GetDatabase(); + try { - _logger.Debug("Releasing MainDom, updating row, new application is booting."); - db.Execute($"UPDATE umbracoKeyValue SET [value] = [value] + '{UpdatedSuffix}' WHERE [key] = @key", new { key = MainDomKey }); + db.BeginTransaction(IsolationLevel.ReadCommitted); + + // get a write lock + _sqlServerSyntax.WriteLock(db, Constants.Locks.MainDom); + + // When we are disposed, it means we have released the MainDom lock + // and called all MainDom release callbacks, in this case + // if another maindom is actually coming online we need + // to signal to the MainDom coming online that we have shutdown. + // To do that, we update the existing main dom DB record with a suffixed "_updated" string. + // Otherwise, if we are just shutting down, we want to just delete the row. + if (_mainDomChanging) + { + _logger.Debug("Releasing MainDom, updating row, new application is booting."); + db.Execute($"UPDATE umbracoKeyValue SET [value] = [value] + '{UpdatedSuffix}' WHERE [key] = @key", new { key = MainDomKey }); + } + else + { + _logger.Debug("Releasing MainDom, deleting row, application is shutting down."); + db.Execute("DELETE FROM umbracoKeyValue WHERE [key] = @key", new { key = MainDomKey }); + } } - else + catch (Exception ex) { - _logger.Debug("Releasing MainDom, deleting row, application is shutting down."); - db.Execute("DELETE FROM umbracoKeyValue WHERE [key] = @key", new { key = MainDomKey }); + ResetDatabase(); + _logger.Error(ex, "Unexpected error during dipsose."); + _hasError = true; + } + finally + { + db?.CompleteTransaction(); + ResetDatabase(); } - } - catch (Exception ex) - { - ResetDatabase(); - _logger.Error(ex, "Unexpected error during dipsose."); - _hasError = true; - } - finally - { - db?.CompleteTransaction(); - ResetDatabase(); } } } diff --git a/src/Umbraco.Infrastructure/Scheduling/ScheduledPublishing.cs b/src/Umbraco.Infrastructure/Scheduling/ScheduledPublishing.cs index fea16999fd..6eb117b677 100644 --- a/src/Umbraco.Infrastructure/Scheduling/ScheduledPublishing.cs +++ b/src/Umbraco.Infrastructure/Scheduling/ScheduledPublishing.cs @@ -70,7 +70,8 @@ namespace Umbraco.Web.Scheduling // but then what should be its "scope"? could we attach it to scopes? // - and we should definitively *not* have to flush it here (should be auto) // - using (var contextReference = _umbracoContextFactory.EnsureUmbracoContext()) + using ( + var contextReference = _umbracoContextFactory.EnsureUmbracoContext()) { try { diff --git a/src/Umbraco.Infrastructure/Search/ExamineComposer.cs b/src/Umbraco.Infrastructure/Search/ExamineComposer.cs index fe3c604f8b..e22cef685a 100644 --- a/src/Umbraco.Infrastructure/Search/ExamineComposer.cs +++ b/src/Umbraco.Infrastructure/Search/ExamineComposer.cs @@ -4,6 +4,7 @@ using Umbraco.Core; using Umbraco.Core.Composing; using Umbraco.Core.Models; using Umbraco.Core.PropertyEditors; +using Umbraco.Core.Scoping; using Umbraco.Core.Services; using Umbraco.Core.Strings; using Umbraco.Examine; @@ -30,13 +31,14 @@ namespace Umbraco.Web.Search composition.Register(Lifetime.Singleton); composition.RegisterUnique(); - composition.RegisterUnique(); + composition.RegisterUnique(); composition.RegisterUnique(factory => new ContentValueSetBuilder( factory.GetInstance(), factory.GetInstance(), factory.GetInstance(), factory.GetInstance(), + factory.GetInstance(), true)); composition.RegisterUnique(factory => new ContentValueSetBuilder( @@ -44,6 +46,7 @@ namespace Umbraco.Web.Search factory.GetInstance(), factory.GetInstance(), factory.GetInstance(), + factory.GetInstance(), false)); composition.RegisterUnique, MediaValueSetBuilder>(); composition.RegisterUnique, MemberValueSetBuilder>(); diff --git a/src/Umbraco.PublishedCache.NuCache/DataSource/DatabaseDataSource.cs b/src/Umbraco.PublishedCache.NuCache/DataSource/DatabaseDataSource.cs index fcab4bb5df..afcae60cdb 100644 --- a/src/Umbraco.PublishedCache.NuCache/DataSource/DatabaseDataSource.cs +++ b/src/Umbraco.PublishedCache.NuCache/DataSource/DatabaseDataSource.cs @@ -19,6 +19,8 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource // provides efficient database access for NuCache internal class DatabaseDataSource : IDataSource { + private const int PageSize = 500; + private readonly ILogger _logger; public DatabaseDataSource(ILogger logger) @@ -85,33 +87,43 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource .Where(x => x.NodeObjectType == Constants.ObjectTypes.Document && !x.Trashed) .OrderBy(x => x.Level, x => x.ParentId, x => x.SortOrder); - return scope.Database.Query(sql).Select(CreateContentNodeKit); + // We need to page here. We don't want to iterate over every single row in one connection cuz this can cause an SQL Timeout. + // We also want to read with a db reader and not load everything into memory, QueryPaged lets us do that. + + foreach (var row in scope.Database.QueryPaged(PageSize, sql)) + yield return CreateContentNodeKit(row); } public IEnumerable GetBranchContentSources(IScope scope, int id) { var syntax = scope.SqlContext.SqlSyntax; - var sql = ContentSourcesSelect(scope, s => s + var sql = ContentSourcesSelect(scope, + s => s.InnerJoin("x").On((left, right) => left.NodeId == right.NodeId || SqlText(left.Path, right.Path, (lp, rp) => $"({lp} LIKE {syntax.GetConcat(rp, "',%'")})"), aliasRight: "x")) + .Where(x => x.NodeObjectType == Constants.ObjectTypes.Document && !x.Trashed) + .Where(x => x.NodeId == id, "x") + .OrderBy(x => x.Level, x => x.ParentId, x => x.SortOrder); - .InnerJoin("x").On((left, right) => left.NodeId == right.NodeId || SqlText(left.Path, right.Path, (lp, rp) => $"({lp} LIKE {syntax.GetConcat(rp, "',%'")})"), aliasRight: "x")) + // We need to page here. We don't want to iterate over every single row in one connection cuz this can cause an SQL Timeout. + // We also want to read with a db reader and not load everything into memory, QueryPaged lets us do that. - .Where(x => x.NodeObjectType == Constants.ObjectTypes.Document && !x.Trashed) - .Where(x => x.NodeId == id, "x") - .OrderBy(x => x.Level, x => x.ParentId, x => x.SortOrder); - - return scope.Database.Query(sql).Select(CreateContentNodeKit); + foreach (var row in scope.Database.QueryPaged(PageSize, sql)) + yield return CreateContentNodeKit(row); } public IEnumerable GetTypeContentSources(IScope scope, IEnumerable ids) { - if (!ids.Any()) return Enumerable.Empty(); + if (!ids.Any()) yield break; var sql = ContentSourcesSelect(scope) .Where(x => x.NodeObjectType == Constants.ObjectTypes.Document && !x.Trashed) .WhereIn(x => x.ContentTypeId, ids) .OrderBy(x => x.Level, x => x.ParentId, x => x.SortOrder); - return scope.Database.Query(sql).Select(CreateContentNodeKit); + // We need to page here. We don't want to iterate over every single row in one connection cuz this can cause an SQL Timeout. + // We also want to read with a db reader and not load everything into memory, QueryPaged lets us do that. + + foreach (var row in scope.Database.QueryPaged(PageSize, sql)) + yield return CreateContentNodeKit(row); } private Sql MediaSourcesSelect(IScope scope, Func, Sql> joins = null) @@ -122,11 +134,8 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource x => Alias(x.Level, "Level"), x => Alias(x.Path, "Path"), x => Alias(x.SortOrder, "SortOrder"), x => Alias(x.ParentId, "ParentId"), x => Alias(x.CreateDate, "CreateDate"), x => Alias(x.UserId, "CreatorId")) .AndSelect(x => Alias(x.ContentTypeId, "ContentTypeId")) - .AndSelect(x => Alias(x.Id, "VersionId"), x => Alias(x.Text, "EditName"), x => Alias(x.VersionDate, "EditVersionDate"), x => Alias(x.UserId, "EditWriterId")) - .AndSelect("nuEdit", x => Alias(x.Data, "EditData")) - .From(); if (joins != null) @@ -134,9 +143,7 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource sql = sql .InnerJoin().On((left, right) => left.NodeId == right.NodeId) - .InnerJoin().On((left, right) => left.NodeId == right.NodeId && right.Current) - .LeftJoin("nuEdit").On((left, right) => left.NodeId == right.NodeId && !right.Published, aliasRight: "nuEdit"); return sql; @@ -158,33 +165,43 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource .Where(x => x.NodeObjectType == Constants.ObjectTypes.Media && !x.Trashed) .OrderBy(x => x.Level, x => x.ParentId, x => x.SortOrder); - return scope.Database.Query(sql).Select(CreateMediaNodeKit); + // We need to page here. We don't want to iterate over every single row in one connection cuz this can cause an SQL Timeout. + // We also want to read with a db reader and not load everything into memory, QueryPaged lets us do that. + + foreach (var row in scope.Database.QueryPaged(PageSize, sql)) + yield return CreateMediaNodeKit(row); } public IEnumerable GetBranchMediaSources(IScope scope, int id) { var syntax = scope.SqlContext.SqlSyntax; - var sql = MediaSourcesSelect(scope, s => s - - .InnerJoin("x").On((left, right) => left.NodeId == right.NodeId || SqlText(left.Path, right.Path, (lp, rp) => $"({lp} LIKE {syntax.GetConcat(rp, "',%'")})"), aliasRight: "x")) - + var sql = MediaSourcesSelect(scope, + s => s.InnerJoin("x").On((left, right) => left.NodeId == right.NodeId || SqlText(left.Path, right.Path, (lp, rp) => $"({lp} LIKE {syntax.GetConcat(rp, "',%'")})"), aliasRight: "x")) .Where(x => x.NodeObjectType == Constants.ObjectTypes.Media && !x.Trashed) .Where(x => x.NodeId == id, "x") .OrderBy(x => x.Level, x => x.ParentId, x => x.SortOrder); - return scope.Database.Query(sql).Select(CreateMediaNodeKit); + // We need to page here. We don't want to iterate over every single row in one connection cuz this can cause an SQL Timeout. + // We also want to read with a db reader and not load everything into memory, QueryPaged lets us do that. + + foreach (var row in scope.Database.QueryPaged(PageSize, sql)) + yield return CreateMediaNodeKit(row); } public IEnumerable GetTypeMediaSources(IScope scope, IEnumerable ids) { - if (!ids.Any()) return Enumerable.Empty(); + if (!ids.Any()) yield break; var sql = MediaSourcesSelect(scope) .Where(x => x.NodeObjectType == Constants.ObjectTypes.Media && !x.Trashed) .WhereIn(x => x.ContentTypeId, ids) .OrderBy(x => x.Level, x => x.ParentId, x => x.SortOrder); - return scope.Database.Query(sql).Select(CreateMediaNodeKit); + // We need to page here. We don't want to iterate over every single row in one connection cuz this can cause an SQL Timeout. + // We also want to read with a db reader and not load everything into memory, QueryPaged lets us do that. + + foreach (var row in scope.Database.QueryPaged(PageSize, sql)) + yield return CreateMediaNodeKit(row); } private ContentNodeKit CreateContentNodeKit(ContentSourceDto dto) diff --git a/src/Umbraco.PublishedCache.NuCache/PublishedSnapshotService.cs b/src/Umbraco.PublishedCache.NuCache/PublishedSnapshotService.cs index 09f486b5d9..ddcaf7de79 100644 --- a/src/Umbraco.PublishedCache.NuCache/PublishedSnapshotService.cs +++ b/src/Umbraco.PublishedCache.NuCache/PublishedSnapshotService.cs @@ -4,6 +4,7 @@ using System.Globalization; using System.IO; using System.Linq; using System.Threading; +using System.Threading.Tasks; using CSharpTest.Net.Collections; using Newtonsoft.Json; using Umbraco.Core; @@ -885,12 +886,37 @@ namespace Umbraco.Web.PublishedCache.NuCache //into a new DLL for the application which includes both content types and media types. //Since the models in the cache are based on these actual classes, all of the objects in the cache need to be updated //to use the newest version of the class. - using (_contentStore.GetScopedWriteLock(_scopeProvider)) - using (_mediaStore.GetScopedWriteLock(_scopeProvider)) - { - NotifyLocked(new[] { new ContentCacheRefresher.JsonPayload(0, null, TreeChangeTypes.RefreshAll) }, out var draftChanged, out var publishedChanged); - NotifyLocked(new[] { new MediaCacheRefresher.JsonPayload(0, null, TreeChangeTypes.RefreshAll) }, out var anythingChanged); - } + + // NOTE: Ideally this can be run on background threads here which would prevent blocking the UI + // as is the case when saving a content type. Intially one would think that it won't be any different + // between running this here or in another background thread immediately after with regards to how the + // UI will respond because we already know between calling `WithSafeLiveFactoryReset` to reset the PureLive models + // and this code here, that many front-end requests could be attempted to be processed. If that is the case, those pages are going to get a + // model binding error and our ModelBindingExceptionFilter is going to to its magic to reload those pages so the end user is none the wiser. + // So whether or not this executes 'here' or on a background thread immediately wouldn't seem to make any difference except that we can return + // execution to the UI sooner. + // BUT!... there is a difference IIRC. There is still execution logic that continues after this call on this thread with the cache refreshers + // and those cache refreshers need to have the up-to-date data since other user cache refreshers will be expecting the data to be 'live'. If + // we ran this on a background thread then those cache refreshers are going to not get 'live' data when they query the content cache which + // they require. + + // These can be run side by side in parallel. + + Parallel.Invoke( + () => + { + using (_contentStore.GetScopedWriteLock(_scopeProvider)) + { + NotifyLocked(new[] { new ContentCacheRefresher.JsonPayload(0, null, TreeChangeTypes.RefreshAll) }, out _, out _); + } + }, + () => + { + using (_mediaStore.GetScopedWriteLock(_scopeProvider)) + { + NotifyLocked(new[] { new MediaCacheRefresher.JsonPayload(0, null, TreeChangeTypes.RefreshAll) }, out _); + } + }); } ((PublishedSnapshot)CurrentPublishedSnapshot)?.Resync(); diff --git a/src/Umbraco.Tests/Services/UserServiceTests.cs b/src/Umbraco.Tests/Services/UserServiceTests.cs index 876928514c..dbd71870d4 100644 --- a/src/Umbraco.Tests/Services/UserServiceTests.cs +++ b/src/Umbraco.Tests/Services/UserServiceTests.cs @@ -924,6 +924,24 @@ namespace Umbraco.Tests.Services Assert.AreEqual(user.Id, profile.Id); } + [Test] + public void Get_By_Profile_Id_Must_return_null_if_user_not_exists() + { + var profile = ServiceContext.UserService.GetProfileById(42); + + // Assert + Assert.IsNull(profile); + } + + [Test] + public void GetProfilesById_Must_empty_if_users_not_exists() + { + var profiles = ServiceContext.UserService.GetProfilesById(42); + + // Assert + CollectionAssert.IsEmpty(profiles); + } + [Test] public void Get_User_By_Username() { diff --git a/src/Umbraco.Tests/UmbracoExamine/IndexInitializer.cs b/src/Umbraco.Tests/UmbracoExamine/IndexInitializer.cs index b97d3de141..b4ff26d760 100644 --- a/src/Umbraco.Tests/UmbracoExamine/IndexInitializer.cs +++ b/src/Umbraco.Tests/UmbracoExamine/IndexInitializer.cs @@ -33,16 +33,23 @@ namespace Umbraco.Tests.UmbracoExamine /// internal static class IndexInitializer { - public static ContentValueSetBuilder GetContentValueSetBuilder(PropertyEditorCollection propertyEditors, bool publishedValuesOnly) + public static ContentValueSetBuilder GetContentValueSetBuilder(PropertyEditorCollection propertyEditors, IScopeProvider scopeProvider, bool publishedValuesOnly) { - var contentValueSetBuilder = new ContentValueSetBuilder(propertyEditors, new UrlSegmentProviderCollection(new[] { new DefaultUrlSegmentProvider(TestHelper.ShortStringHelper) }), GetMockUserService(), TestHelper.ShortStringHelper, publishedValuesOnly); + var contentValueSetBuilder = new ContentValueSetBuilder( + propertyEditors, + new UrlSegmentProviderCollection(new[] { new DefaultUrlSegmentProvider(TestHelper.ShortStringHelper) }), + GetMockUserService(), + TestHelper.ShortStringHelper, + scopeProvider, + publishedValuesOnly); + return contentValueSetBuilder; } - public static ContentIndexPopulator GetContentIndexRebuilder(PropertyEditorCollection propertyEditors, IContentService contentService, ISqlContext sqlContext, bool publishedValuesOnly) + public static ContentIndexPopulator GetContentIndexRebuilder(PropertyEditorCollection propertyEditors, IContentService contentService, IScopeProvider scopeProvider, bool publishedValuesOnly) { - var contentValueSetBuilder = GetContentValueSetBuilder(propertyEditors, publishedValuesOnly); - var contentIndexDataSource = new ContentIndexPopulator(true, null, contentService, sqlContext, contentValueSetBuilder); + var contentValueSetBuilder = GetContentValueSetBuilder(propertyEditors, scopeProvider, publishedValuesOnly); + var contentIndexDataSource = new ContentIndexPopulator(true, null, contentService, scopeProvider.SqlContext, contentValueSetBuilder); return contentIndexDataSource; } diff --git a/src/Umbraco.Tests/UmbracoExamine/IndexTest.cs b/src/Umbraco.Tests/UmbracoExamine/IndexTest.cs index f4ec4dbd1e..2e3419720a 100644 --- a/src/Umbraco.Tests/UmbracoExamine/IndexTest.cs +++ b/src/Umbraco.Tests/UmbracoExamine/IndexTest.cs @@ -30,7 +30,7 @@ namespace Umbraco.Tests.UmbracoExamine [Test] public void Index_Property_Data_With_Value_Indexer() { - var contentValueSetBuilder = IndexInitializer.GetContentValueSetBuilder(Factory.GetInstance(), false); + var contentValueSetBuilder = IndexInitializer.GetContentValueSetBuilder(Factory.GetInstance(), ScopeProvider, false); using (var luceneDir = new RandomIdRamDirectory()) using (var indexer = IndexInitializer.GetUmbracoIndexer(ProfilingLogger, HostingEnvironment, RuntimeState, luceneDir, @@ -122,7 +122,7 @@ namespace Umbraco.Tests.UmbracoExamine [Test] public void Rebuild_Index() { - var contentRebuilder = IndexInitializer.GetContentIndexRebuilder(Factory.GetInstance(), IndexInitializer.GetMockContentService(), ScopeProvider.SqlContext, false); + var contentRebuilder = IndexInitializer.GetContentIndexRebuilder(Factory.GetInstance(), IndexInitializer.GetMockContentService(), ScopeProvider, false); var mediaRebuilder = IndexInitializer.GetMediaIndexRebuilder(Factory.GetInstance(), IndexInitializer.GetMockMediaService()); using (var luceneDir = new RandomIdRamDirectory()) @@ -150,7 +150,7 @@ namespace Umbraco.Tests.UmbracoExamine [Test] public void Index_Protected_Content_Not_Indexed() { - var rebuilder = IndexInitializer.GetContentIndexRebuilder(Factory.GetInstance(), IndexInitializer.GetMockContentService(), ScopeProvider.SqlContext, false); + var rebuilder = IndexInitializer.GetContentIndexRebuilder(Factory.GetInstance(), IndexInitializer.GetMockContentService(), ScopeProvider, false); using (var luceneDir = new RandomIdRamDirectory()) @@ -275,7 +275,7 @@ namespace Umbraco.Tests.UmbracoExamine [Test] public void Index_Reindex_Content() { - var rebuilder = IndexInitializer.GetContentIndexRebuilder(Factory.GetInstance(), IndexInitializer.GetMockContentService(), ScopeProvider.SqlContext, false); + var rebuilder = IndexInitializer.GetContentIndexRebuilder(Factory.GetInstance(), IndexInitializer.GetMockContentService(), ScopeProvider, false); using (var luceneDir = new RandomIdRamDirectory()) using (var indexer = IndexInitializer.GetUmbracoIndexer(ProfilingLogger, HostingEnvironment, RuntimeState, luceneDir, validator: new ContentValueSetValidator(false))) @@ -316,7 +316,7 @@ namespace Umbraco.Tests.UmbracoExamine public void Index_Delete_Index_Item_Ensure_Heirarchy_Removed() { - var rebuilder = IndexInitializer.GetContentIndexRebuilder(Factory.GetInstance(), IndexInitializer.GetMockContentService(), ScopeProvider.SqlContext, false); + var rebuilder = IndexInitializer.GetContentIndexRebuilder(Factory.GetInstance(), IndexInitializer.GetMockContentService(), ScopeProvider, false); using (var luceneDir = new RandomIdRamDirectory()) using (var indexer = IndexInitializer.GetUmbracoIndexer(ProfilingLogger, HostingEnvironment, RuntimeState, luceneDir)) diff --git a/src/Umbraco.Tests/UmbracoExamine/SearchTests.cs b/src/Umbraco.Tests/UmbracoExamine/SearchTests.cs index d722bf89ae..d30aec874e 100644 --- a/src/Umbraco.Tests/UmbracoExamine/SearchTests.cs +++ b/src/Umbraco.Tests/UmbracoExamine/SearchTests.cs @@ -55,7 +55,7 @@ namespace Umbraco.Tests.UmbracoExamine allRecs); var propertyEditors = Factory.GetInstance(); - var rebuilder = IndexInitializer.GetContentIndexRebuilder(propertyEditors, contentService, ScopeProvider.SqlContext, true); + var rebuilder = IndexInitializer.GetContentIndexRebuilder(propertyEditors, contentService, ScopeProvider, true); using (var luceneDir = new RandomIdRamDirectory()) using (var indexer = IndexInitializer.GetUmbracoIndexer(ProfilingLogger, HostingEnvironment, RuntimeState, luceneDir)) diff --git a/src/Umbraco.Web.Common/Extensions/UmbracoCoreServiceCollectionExtensions.cs b/src/Umbraco.Web.Common/Extensions/UmbracoCoreServiceCollectionExtensions.cs index 3facf1b77f..0fe784f421 100644 --- a/src/Umbraco.Web.Common/Extensions/UmbracoCoreServiceCollectionExtensions.cs +++ b/src/Umbraco.Web.Common/Extensions/UmbracoCoreServiceCollectionExtensions.cs @@ -84,7 +84,6 @@ namespace Umbraco.Web.Common.Extensions IHttpContextAccessor httpContextAccessor = new HttpContextAccessor(); services.AddSingleton(httpContextAccessor); - var requestCache = new GenericDictionaryRequestAppCache(() => httpContextAccessor.HttpContext.Items); services.AddUmbracoCore(webHostEnvironment, diff --git a/src/Umbraco.Web.UI.Client/src/common/services/events.service.js b/src/Umbraco.Web.UI.Client/src/common/services/events.service.js index 51f63e6787..965ac3d635 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/events.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/events.service.js @@ -1,7 +1,7 @@ /** Used to broadcast and listen for global events and allow the ability to add async listeners to the callbacks */ /* - Core app events: + Core app events: app.ready app.authenticated @@ -12,9 +12,9 @@ */ function eventsService($q, $rootScope) { - + return { - + /** raise an event with a given name */ emit: function (name, args) { diff --git a/src/Umbraco.Web.UI.Client/src/common/services/user.service.js b/src/Umbraco.Web.UI.Client/src/common/services/user.service.js index afd7b606e7..5b4e516289 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/user.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/user.service.js @@ -185,7 +185,19 @@ angular.module('umbraco.services') authenticate: function (login, password) { return authResource.performLogin(login, password) - .then(this.setAuthenticationSuccessful); + .then(function(data) { + + // Check if user has a start node set. + if(data.startContentIds.length === 0 && data.startMediaIds.length === 0){ + var errorMsg = "User has no start-nodes"; + var result = { errorMsg: errorMsg, user: data, authenticated: false, lastUserId: lastUserId, loginType: "credentials" }; + eventsService.emit("app.notAuthenticated", result); + throw result; + } + + return data; + + }).then(this.setAuthenticationSuccessful); }, setAuthenticationSuccessful: function (data) { diff --git a/src/Umbraco.Web.UI.Client/src/main.controller.js b/src/Umbraco.Web.UI.Client/src/main.controller.js index 198ac132c1..81eadf150f 100644 --- a/src/Umbraco.Web.UI.Client/src/main.controller.js +++ b/src/Umbraco.Web.UI.Client/src/main.controller.js @@ -72,6 +72,7 @@ function MainController($scope, $location, appState, treeService, notificationsS $scope.authenticated = null; $scope.user = null; const isTimedOut = data && data.isTimedOut ? true : false; + $scope.showLoginScreen(isTimedOut); // Remove the localstorage items for tours shown diff --git a/src/Umbraco.Web/Security/BackOfficeSignInManager.cs b/src/Umbraco.Web/Security/BackOfficeSignInManager.cs index fe5b061d15..6e0ffac590 100644 --- a/src/Umbraco.Web/Security/BackOfficeSignInManager.cs +++ b/src/Umbraco.Web/Security/BackOfficeSignInManager.cs @@ -123,7 +123,9 @@ namespace Umbraco.Web.Security { _logger.WriteCore(TraceEventType.Information, 0, $"Login attempt failed for username {userName} from IP address {_request.RemoteIpAddress}, no content and/or media start nodes could be found for any of the user's groups", null, null); - return SignInStatus.Failure; + + // We will say its a sucessful login which it is, but they have no node access + return SignInStatus.Success; } } diff --git a/src/Umbraco.Web/UmbracoApplicationBase.cs b/src/Umbraco.Web/UmbracoApplicationBase.cs index 1f90bc7d13..799b627f2c 100644 --- a/src/Umbraco.Web/UmbracoApplicationBase.cs +++ b/src/Umbraco.Web/UmbracoApplicationBase.cs @@ -73,7 +73,7 @@ namespace Umbraco.Web } protected UmbracoApplicationBase(ILogger logger, Configs configs, IIOHelper ioHelper, IProfiler profiler, IHostingEnvironment hostingEnvironment, IBackOfficeInfo backOfficeInfo) - { + { if (!Umbraco.Composing.Current.IsInitialized) { Logger = logger;