diff --git a/.gitignore b/.gitignore index 9cea2d4e1005..c69474ac30ba 100644 --- a/.gitignore +++ b/.gitignore @@ -84,6 +84,7 @@ src/Umbraco.Web.UI/wwwroot/[Uu]mbraco/js/* src/Umbraco.Web.UI/wwwroot/[Uu]mbraco/lib/* src/Umbraco.Web.UI/wwwroot/[Uu]mbraco/views/* src/Umbraco.Web.UI/wwwroot/Media/* +src/Umbraco.Web.UI/Smidge/ # Tests cypress.env.json diff --git a/src/Umbraco.Core/Configuration/EntryAssemblyMetadata.cs b/src/Umbraco.Core/Configuration/EntryAssemblyMetadata.cs new file mode 100644 index 000000000000..b6b9f067b9a6 --- /dev/null +++ b/src/Umbraco.Core/Configuration/EntryAssemblyMetadata.cs @@ -0,0 +1,24 @@ +using System.Reflection; + +namespace Umbraco.Cms.Core.Configuration +{ + internal class EntryAssemblyMetadata : IEntryAssemblyMetadata + { + public EntryAssemblyMetadata() + { + var entryAssembly = Assembly.GetEntryAssembly(); + + Name = entryAssembly + ?.GetName() + ?.Name ?? string.Empty; + + InformationalVersion = entryAssembly + ?.GetCustomAttribute() + ?.InformationalVersion ?? string.Empty; + } + + public string Name { get; } + + public string InformationalVersion { get; } + } +} diff --git a/src/Umbraco.Core/Configuration/IEntryAssemblyMetadata.cs b/src/Umbraco.Core/Configuration/IEntryAssemblyMetadata.cs new file mode 100644 index 000000000000..09ea5058df35 --- /dev/null +++ b/src/Umbraco.Core/Configuration/IEntryAssemblyMetadata.cs @@ -0,0 +1,18 @@ +namespace Umbraco.Cms.Core.Configuration +{ + /// + /// Provides metadata about the entry assembly. + /// + public interface IEntryAssemblyMetadata + { + /// + /// Gets the Name of entry assembly. + /// + public string Name { get; } + + /// + /// Gets the InformationalVersion string for entry assembly. + /// + public string InformationalVersion { get; } + } +} diff --git a/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.cs b/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.cs index 5aa62eae1982..36d79185317d 100644 --- a/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.cs +++ b/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.cs @@ -163,6 +163,7 @@ private void AddCoreServices() Services.AddUnique(factory => factory.GetRequiredService().RequestCache); Services.AddUnique(); Services.AddUnique(); + Services.AddUnique(); this.AddAllCoreCollectionBuilders(); this.AddNotificationHandler(); diff --git a/src/Umbraco.Core/WebAssets/IRuntimeMinifier.cs b/src/Umbraco.Core/WebAssets/IRuntimeMinifier.cs index 7ca1cd883b78..a3b88633b617 100644 --- a/src/Umbraco.Core/WebAssets/IRuntimeMinifier.cs +++ b/src/Umbraco.Core/WebAssets/IRuntimeMinifier.cs @@ -88,7 +88,18 @@ public interface IRuntimeMinifier /// /// Ensures that all runtime minifications are refreshed on next request. E.g. Clearing cache. /// + /// + /// + /// No longer necessary, invalidation occurs automatically if any of the following occur. + /// + /// + /// Your sites assembly information version changes. + /// Umbraco.Cms.Core assembly information version changes. + /// RuntimeMinificationSettings Version string changes. + /// + /// for further details. + /// + [Obsolete("Invalidation is handled automatically. Scheduled for removal V11.")] void Reset(); - } } diff --git a/src/Umbraco.Web.BackOffice/Install/InstallController.cs b/src/Umbraco.Web.BackOffice/Install/InstallController.cs index a773a6f5a1a2..17aba2b27ee1 100644 --- a/src/Umbraco.Web.BackOffice/Install/InstallController.cs +++ b/src/Umbraco.Web.BackOffice/Install/InstallController.cs @@ -72,9 +72,6 @@ public async Task Index() // TODO: Update for package migrations if (_runtime.Level == RuntimeLevel.Upgrade) { - // Update ClientDependency version and delete its temp directories to make sure we get fresh caches - _runtimeMinifier.Reset(); - var authResult = await this.AuthenticateBackOfficeAsync(); if (!authResult.Succeeded) diff --git a/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilderExtensions.cs b/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilderExtensions.cs index 84538c9310dc..c81419a3f2c8 100644 --- a/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilderExtensions.cs +++ b/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilderExtensions.cs @@ -18,6 +18,7 @@ using Microsoft.Extensions.Logging; using Serilog; using Smidge; +using Smidge.Cache; using Smidge.FileProcessors; using Smidge.InMemory; using Smidge.Nuglify; @@ -274,6 +275,7 @@ public static IUmbracoBuilder AddRuntimeMinifier(this IUmbracoBuilder builder) new[] { "/App_Plugins/**/*.js", "/App_Plugins/**/*.css" })); }); + builder.Services.AddUnique(); builder.Services.AddSmidge(builder.Config.GetSection(Constants.Configuration.ConfigRuntimeMinification)); // Replace the Smidge request helper, in order to discourage the use of brotli since it's super slow builder.Services.AddUnique(); diff --git a/src/Umbraco.Web.Common/RuntimeMinification/SmidgeRuntimeMinifier.cs b/src/Umbraco.Web.Common/RuntimeMinification/SmidgeRuntimeMinifier.cs index 96188ba08c33..6e24c06b8edb 100644 --- a/src/Umbraco.Web.Common/RuntimeMinification/SmidgeRuntimeMinifier.cs +++ b/src/Umbraco.Web.Common/RuntimeMinification/SmidgeRuntimeMinifier.cs @@ -24,7 +24,6 @@ public class SmidgeRuntimeMinifier : IRuntimeMinifier private readonly IHostingEnvironment _hostingEnvironment; private readonly IConfigManipulator _configManipulator; private readonly CacheBusterResolver _cacheBusterResolver; - private readonly RuntimeMinificationSettings _runtimeMinificationSettings; private readonly IBundleManager _bundles; private readonly SmidgeHelperAccessor _smidge; @@ -53,7 +52,6 @@ public SmidgeRuntimeMinifier( _hostingEnvironment = hostingEnvironment; _configManipulator = configManipulator; _cacheBusterResolver = cacheBusterResolver; - _runtimeMinificationSettings = runtimeMinificationSettings.Value; _jsMinPipeline = new Lazy(() => _bundles.PipelineFactory.Create(typeof(JsMinifier))); _cssMinPipeline = new Lazy(() => _bundles.PipelineFactory.Create(typeof(NuglifyCss))); @@ -76,10 +74,10 @@ public SmidgeRuntimeMinifier( return defaultCss; }); - Type cacheBusterType = _runtimeMinificationSettings.CacheBuster switch + Type cacheBusterType = runtimeMinificationSettings.Value.CacheBuster switch { RuntimeMinificationCacheBuster.AppDomain => typeof(AppDomainLifetimeCacheBuster), - RuntimeMinificationCacheBuster.Version => typeof(ConfigCacheBuster), + RuntimeMinificationCacheBuster.Version => typeof(UmbracoSmidgeConfigCacheBuster), RuntimeMinificationCacheBuster.Timestamp => typeof(TimestampCacheBuster), _ => throw new NotImplementedException() }; @@ -169,18 +167,12 @@ public async Task MinifyAsync(string fileContent, AssetType assetType) } } - /// - /// - /// Smidge uses the version number as cache buster (configurable). - /// We therefore can reset, by updating the version number in config - /// + [Obsolete("Invalidation is handled automatically. Scheduled for removal V11.")] public void Reset() { var version = DateTime.UtcNow.Ticks.ToString(); _configManipulator.SaveConfigValue(Cms.Core.Constants.Configuration.ConfigRuntimeMinificationVersion, version.ToString()); } - - } } diff --git a/src/Umbraco.Web.Common/RuntimeMinification/UmbracoSmidgeConfigCacheBuster.cs b/src/Umbraco.Web.Common/RuntimeMinification/UmbracoSmidgeConfigCacheBuster.cs new file mode 100644 index 000000000000..c32320414819 --- /dev/null +++ b/src/Umbraco.Web.Common/RuntimeMinification/UmbracoSmidgeConfigCacheBuster.cs @@ -0,0 +1,77 @@ +using System; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Smidge; +using Smidge.Cache; +using Umbraco.Cms.Core.Configuration; +using Umbraco.Cms.Core.Configuration.Models; +using Umbraco.Extensions; + +namespace Umbraco.Cms.Web.Common.RuntimeMinification +{ + /// + /// Constructs a cache buster string with sensible defaults. + /// + /// + /// + /// Had planned on handling all of this in SmidgeRuntimeMinifier, but that only handles some urls. + /// + /// + /// A lot of the work is delegated e.g. to + /// which doesn't care about the cache buster string in our classes only the value returned by the resolved ICacheBuster. + /// + /// + /// Then I thought fine I'll just use a IConfigureOptions to tweak upstream , but that only cares about the + /// class we instantiate and pass through in + /// + /// + /// So here we are, create our own to ensure we cache bust in a reasonable fashion. + /// + ///

+ /// + /// Note that this class makes some other bits of code pretty redundant e.g. will + /// concatenate version with CacheBuster value and hash again, but there's no real harm so can think about that later. + /// + ///
+ internal class UmbracoSmidgeConfigCacheBuster : ICacheBuster + { + private readonly IOptions _runtimeMinificationSettings; + private readonly IUmbracoVersion _umbracoVersion; + private readonly IEntryAssemblyMetadata _entryAssemblyMetadata; + + private string _cacheBusterValue; + + public UmbracoSmidgeConfigCacheBuster( + IOptions runtimeMinificationSettings, + IUmbracoVersion umbracoVersion, + IEntryAssemblyMetadata entryAssemblyMetadata) + { + _runtimeMinificationSettings = runtimeMinificationSettings ?? throw new ArgumentNullException(nameof(runtimeMinificationSettings)); + _umbracoVersion = umbracoVersion ?? throw new ArgumentNullException(nameof(umbracoVersion)); + _entryAssemblyMetadata = entryAssemblyMetadata ?? throw new ArgumentNullException(nameof(entryAssemblyMetadata)); + } + + private string CacheBusterValue + { + get + { + if (_cacheBusterValue != null) + { + return _cacheBusterValue; + } + + // Assembly Name adds a bit of uniqueness across sites when version missing from config. + // Adds a bit of security through obscurity that was asked for in standup. + var prefix = _runtimeMinificationSettings.Value.Version ?? _entryAssemblyMetadata.Name ?? string.Empty; + var umbracoVersion = _umbracoVersion.SemanticVersion.ToString(); + var downstreamVersion = _entryAssemblyMetadata.InformationalVersion; + + _cacheBusterValue = $"{prefix}_{umbracoVersion}_{downstreamVersion}".GenerateHash(); + + return _cacheBusterValue; + } + } + + public string GetValue() => CacheBusterValue; + } +} diff --git a/tests/.editorconfig b/tests/.editorconfig new file mode 100644 index 000000000000..16b1586955fe --- /dev/null +++ b/tests/.editorconfig @@ -0,0 +1,5 @@ +root = false + +[*.cs] +csharp_style_var_when_type_is_apparent = true:none +csharp_style_var_elsewhere = true:none \ No newline at end of file diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Web.Common/RuntimeMinification/UmbracoSmidgeConfigCacheBusterTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Web.Common/RuntimeMinification/UmbracoSmidgeConfigCacheBusterTests.cs new file mode 100644 index 000000000000..2eab83858725 --- /dev/null +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Web.Common/RuntimeMinification/UmbracoSmidgeConfigCacheBusterTests.cs @@ -0,0 +1,113 @@ +using AutoFixture; +using AutoFixture.AutoMoq; +using AutoFixture.Kernel; +using Microsoft.Extensions.Options; +using Moq; +using NUnit.Framework; +using Smidge; +using Smidge.Cache; +using Umbraco.Cms.Core.Configuration; +using Umbraco.Cms.Core.Configuration.Models; +using Umbraco.Cms.Core.Semver; +using Umbraco.Cms.Web.Common.RuntimeMinification; +using Umbraco.Extensions; + +namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Web.Common.RuntimeMinification +{ + /// + /// UmbracoCustomizations kindly configures an IUmbracoVersion so we need to go verbose without AutoMoqData + /// + [TestFixture] + public class UmbracoSmidgeConfigCacheBusterTests + { + [Test] + public void GetValue_DefaultReleaseSetupWithNoConfiguredVersion_HasSensibleDefaults() + { + var fixture = new Fixture(); + fixture.Customize(new AutoMoqCustomization()); + + var umbracoVersion = fixture.Freeze>(); + var entryAssemblyMetadata = fixture.Freeze>(); + var sut = fixture.Create(); + + umbracoVersion.Setup(x => x.SemanticVersion).Returns(new SemVersion(9, 4, 5, "beta", "41658f99")); + entryAssemblyMetadata.Setup(x => x.Name).Returns("Bills.Brilliant.Bakery"); + entryAssemblyMetadata.Setup(x => x.InformationalVersion).Returns("42.1.2-alpha+41658f99"); + + var result = sut.GetValue(); + + var expected = $"Bills.Brilliant.Bakery_9.4.5-beta+41658f99_42.1.2-alpha+41658f99".GenerateHash(); + Assert.AreEqual(expected, result); + } + + [Test] + public void GetValue_DefaultReleaseSetupWithConfiguredVersion_HasSensibleDefaults() + { + var config = Options.Create(new RuntimeMinificationSettings { Version = "1" }); + var fixture = new Fixture(); + fixture.Customize(new AutoMoqCustomization()); + fixture.Inject(config); + + var umbracoVersion = fixture.Freeze>(); + var entryAssemblyMetadata = fixture.Freeze>(); + var sut = fixture.Create(); + + umbracoVersion.Setup(x => x.SemanticVersion).Returns(new SemVersion(9, 4, 5, "beta", "41658f99")); + entryAssemblyMetadata.Setup(x => x.Name).Returns("Bills.Brilliant.Bakery"); + entryAssemblyMetadata.Setup(x => x.InformationalVersion).Returns("42.1.2-alpha+41658f99"); + + var result = sut.GetValue(); + + var expected = $"1_9.4.5-beta+41658f99_42.1.2-alpha+41658f99".GenerateHash(); + Assert.AreEqual(expected, result); + } + + [Test] + public void GetValue_DefaultReleaseSetupWithNoConfiguredVersion_ChangesWhenUmbracoVersionChanges() + { + var fixture = new Fixture(); + fixture.Customize(new AutoMoqCustomization()); + + var umbracoVersion = fixture.Freeze>(); + var entryAssemblyMetadata = fixture.Freeze>(); + var sut = fixture.Create(); + + umbracoVersion.Setup(x => x.SemanticVersion).Returns(new SemVersion(9, 4, 5, "beta", "41658f99")); + entryAssemblyMetadata.Setup(x => x.Name).Returns("Bills.Brilliant.Bakery"); + entryAssemblyMetadata.Setup(x => x.InformationalVersion).Returns("42.1.2-alpha+41658f99"); + + var before = sut.GetValue(); + + umbracoVersion.Setup(x => x.SemanticVersion).Returns(new SemVersion(9, 5, 0, "beta", "41658f99")); + sut = fixture.Create(); + + var after = sut.GetValue(); + + Assert.AreNotEqual(before, after); + } + + [Test] + public void GetValue_DefaultReleaseSetupWithNoConfiguredVersion_ChangesWhenDownstreamVersionChanges() + { + var fixture = new Fixture(); + fixture.Customize(new AutoMoqCustomization()); + + var umbracoVersion = fixture.Freeze>(); + var entryAssemblyMetadata = fixture.Freeze>(); + var sut = fixture.Create(); + + umbracoVersion.Setup(x => x.SemanticVersion).Returns(new SemVersion(9, 4, 5, "beta", "41658f99")); + entryAssemblyMetadata.Setup(x => x.Name).Returns("Bills.Brilliant.Bakery"); + entryAssemblyMetadata.Setup(x => x.InformationalVersion).Returns("42.1.2-alpha+41658f99"); + + var before = sut.GetValue(); + + entryAssemblyMetadata.Setup(x => x.InformationalVersion).Returns("42.2.0-rc"); + sut = fixture.Create(); + + var after = sut.GetValue(); + + Assert.AreNotEqual(before, after); + } + } +} diff --git a/umbraco.sln.DotSettings b/umbraco.sln.DotSettings index 65d49c5ff2c0..af272e5c8a7b 100644 --- a/umbraco.sln.DotSettings +++ b/umbraco.sln.DotSettings @@ -6,6 +6,7 @@ HINT False Default + True True True True \ No newline at end of file