fix #5005 - adds versioning (ie. fingerprinting) for static assets - core, modules, and themes.

This commit is contained in:
sbwalker
2025-01-27 16:34:47 -05:00
parent 7a9c637e03
commit 153a689bdb
15 changed files with 145 additions and 60 deletions

View File

@ -39,7 +39,7 @@
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<base href="/" />
<link rel="stylesheet" href="css/app.css" />
<link rel="stylesheet" href="css/app.css?v=@_hash" />
@if (_scripts.Contains("PWA Manifest"))
{
<link id="app-manifest" rel="manifest" />
@ -70,15 +70,15 @@
}
<script src="_framework/blazor.web.js"></script>
<script src="js/app.js"></script>
<script src="js/loadjs.min.js"></script>
<script src="js/interop.js"></script>
<script src="js/app.js?v=@_hash"></script>
<script src="js/loadjs.min.js?v=@_hash"></script>
<script src="js/interop.js?v=@_hash"></script>
@((MarkupString)_scripts)
@((MarkupString)_bodyResources)
@if (_renderMode == RenderModes.Static)
{
<page-script src="./js/reload.js"></page-script>
<page-script src="./js/reload.js?v=@_hash"></page-script>
}
}
else
@ -94,6 +94,7 @@
private string _renderMode = RenderModes.Interactive;
private string _runtime = Runtimes.Server;
private bool _prerender = true;
private string _hash = "";
private int _visitorId = -1;
private string _antiForgeryToken = "";
private string _remoteIPAddress = "";
@ -136,6 +137,8 @@
_renderMode = site.RenderMode;
_runtime = site.Runtime;
_prerender = site.Prerender;
_hash = site.Hash;
var modules = new List<Module>();
Route route = new Route(url, alias.Path);
@ -605,13 +608,13 @@
var theme = site.Themes.FirstOrDefault(item => item.Themes.Any(item => item.TypeName == themeType));
if (theme != null)
{
resources = AddResources(resources, theme.Resources, ResourceLevel.Page, alias, "Themes", Utilities.GetTypeName(theme.ThemeName), site.RenderMode);
resources = AddResources(resources, theme.Resources, ResourceLevel.Page, alias, "Themes", Utilities.GetTypeName(theme.ThemeName), theme.Hash, site.RenderMode);
}
else
{
// fallback to default Oqtane theme
theme = site.Themes.FirstOrDefault(item => item.Themes.Any(item => item.TypeName == Constants.DefaultTheme));
resources = AddResources(resources, theme.Resources, ResourceLevel.Page, alias, "Themes", Utilities.GetTypeName(theme.ThemeName), site.RenderMode);
resources = AddResources(resources, theme.Resources, ResourceLevel.Page, alias, "Themes", Utilities.GetTypeName(theme.ThemeName), theme.Hash, site.RenderMode);
}
var type = Type.GetType(themeType);
if (type != null)
@ -619,7 +622,7 @@
var obj = Activator.CreateInstance(type) as IThemeControl;
if (obj != null)
{
resources = AddResources(resources, obj.Resources, ResourceLevel.Page, alias, "Themes", type.Namespace, site.RenderMode);
resources = AddResources(resources, obj.Resources, ResourceLevel.Page, alias, "Themes", type.Namespace, theme.Hash, site.RenderMode);
}
}
// theme settings components are dynamically loaded within the framework Page Management module
@ -629,7 +632,7 @@
if (settingsType != null)
{
var objSettings = Activator.CreateInstance(settingsType) as IModuleControl;
resources = AddResources(resources, objSettings.Resources, ResourceLevel.Module, alias, "Modules", settingsType.Namespace, site.RenderMode);
resources = AddResources(resources, objSettings.Resources, ResourceLevel.Module, alias, "Modules", settingsType.Namespace, theme.Hash, site.RenderMode);
}
}
@ -638,7 +641,7 @@
var typename = "";
if (module.ModuleDefinition != null)
{
resources = AddResources(resources, module.ModuleDefinition.Resources, ResourceLevel.Module, alias, "Modules", Utilities.GetTypeName(module.ModuleDefinition.ModuleDefinitionName), site.RenderMode);
resources = AddResources(resources, module.ModuleDefinition.Resources, ResourceLevel.Module, alias, "Modules", Utilities.GetTypeName(module.ModuleDefinition.ModuleDefinitionName), module.ModuleDefinition.Hash, site.RenderMode);
// handle default action
if (action == Constants.DefaultAction && !string.IsNullOrEmpty(module.ModuleDefinition.DefaultAction))
@ -684,7 +687,7 @@
var moduleobject = Activator.CreateInstance(moduletype) as IModuleControl;
if (moduleobject != null)
{
resources = AddResources(resources, moduleobject.Resources, ResourceLevel.Module, alias, "Modules", moduletype.Namespace, site.RenderMode);
resources = AddResources(resources, moduleobject.Resources, ResourceLevel.Module, alias, "Modules", moduletype.Namespace, module.ModuleDefinition?.Hash, site.RenderMode);
// settings components are dynamically loaded within the framework Settings module
if (action.ToLower() == "settings" && module.ModuleDefinition != null)
@ -705,7 +708,7 @@
if (moduletype != null)
{
moduleobject = Activator.CreateInstance(moduletype) as IModuleControl;
resources = AddResources(resources, moduleobject.Resources, ResourceLevel.Module, alias, "Modules", moduletype.Namespace, site.RenderMode);
resources = AddResources(resources, moduleobject.Resources, ResourceLevel.Module, alias, "Modules", moduletype.Namespace, module.ModuleDefinition?.Hash, site.RenderMode);
}
// container settings component
@ -715,7 +718,7 @@
if (moduletype != null)
{
moduleobject = Activator.CreateInstance(moduletype) as IModuleControl;
resources = AddResources(resources, moduleobject.Resources, ResourceLevel.Module, alias, "Modules", moduletype.Namespace, site.RenderMode);
resources = AddResources(resources, moduleobject.Resources, ResourceLevel.Module, alias, "Modules", moduletype.Namespace, theme.Hash, site.RenderMode);
}
}
}
@ -731,7 +734,7 @@
{
if (module.ModuleDefinition?.Resources != null)
{
resources = AddResources(resources, module.ModuleDefinition.Resources.Where(item => item.ResourceType == ResourceType.Script && item.Level == ResourceLevel.Site).ToList(), ResourceLevel.Module, alias, "Modules", Utilities.GetTypeName(module.ModuleDefinition.ModuleDefinitionName), site.RenderMode);
resources = AddResources(resources, module.ModuleDefinition.Resources.Where(item => item.ResourceType == ResourceType.Script && item.Level == ResourceLevel.Site).ToList(), ResourceLevel.Module, alias, "Modules", Utilities.GetTypeName(module.ModuleDefinition.ModuleDefinitionName), module.ModuleDefinition.Hash, site.RenderMode);
}
}
}
@ -739,7 +742,7 @@
return resources;
}
private List<Resource> AddResources(List<Resource> pageresources, List<Resource> resources, ResourceLevel level, Alias alias, string type, string name, string rendermode)
private List<Resource> AddResources(List<Resource> pageresources, List<Resource> resources, ResourceLevel level, Alias alias, string type, string name, string version, string rendermode)
{
if (resources != null)
{
@ -759,7 +762,7 @@
// ensure resource does not exist already
if (!pageresources.Exists(item => item.Url.ToLower() == resource.Url.ToLower()))
{
pageresources.Add(resource.Clone(level, name));
pageresources.Add(resource.Clone(level, name, version));
}
}
}

View File

@ -286,7 +286,7 @@ namespace Oqtane.Controllers
DateTime lastwritetime = System.IO.File.GetLastWriteTime(filepath);
if (hashfilename)
{
HashedName = GetDeterministicHashCode(filepath).ToString("X8") + "." + lastwritetime.ToString("yyyyMMddHHmmss") + Path.GetExtension(filepath);
HashedName = Utilities.GenerateSimpleHash(filepath) + "." + lastwritetime.ToString("yyyyMMddHHmmss") + Path.GetExtension(filepath);
}
else
{
@ -297,25 +297,5 @@ namespace Oqtane.Controllers
public string FilePath { get; private set; }
public string HashedName { get; private set; }
}
private static int GetDeterministicHashCode(string value)
{
unchecked
{
int hash1 = (5381 << 16) + 5381;
int hash2 = hash1;
for (int i = 0; i < value.Length; i += 2)
{
hash1 = ((hash1 << 5) + hash1) ^ value[i];
if (i == value.Length - 1)
break;
hash2 = ((hash2 << 5) + hash2) ^ value[i + 1];
}
return hash1 + (hash2 * 1566083941);
}
}
}
}

View File

@ -132,7 +132,7 @@ namespace Oqtane.Controllers
if (user != null)
{
email = user.Email;
_configManager.AddOrUpdateSetting("PackageRegistryEmail", email, false);
_configManager.AddOrUpdateSetting("PackageRegistryEmail", email, true);
}
}
}

View File

@ -175,6 +175,12 @@ namespace Oqtane.Infrastructure
installationid = Guid.NewGuid().ToString();
AddOrUpdateSetting("InstallationId", installationid, true);
}
var version = GetSetting("InstallationVersion", "");
if (version != Constants.Version)
{
AddOrUpdateSetting("InstallationVersion", Constants.Version, true);
AddOrUpdateSetting("InstallationDate", DateTime.UtcNow.ToString("yyyyMMddHHmm"), true);
}
return installationid;
}
}

View File

@ -265,6 +265,7 @@ namespace Oqtane.Infrastructure
var installation = IsInstalled();
try
{
UpdateInstallation();
UpdateConnectionString(install.ConnectionString);
UpdateDatabaseType(install.DatabaseType);
@ -491,6 +492,7 @@ namespace Oqtane.Infrastructure
moduleDefinition.Categories = moduledef.Categories;
// update version
moduleDefinition.Version = versions[versions.Length - 1];
moduleDefinition.ModifiedOn = DateTime.UtcNow;
db.Entry(moduleDefinition).State = EntityState.Modified;
db.SaveChanges();
}
@ -666,6 +668,11 @@ namespace Oqtane.Infrastructure
return connectionString;
}
public void UpdateInstallation()
{
_config.GetInstallationId();
}
public void UpdateConnectionString(string connectionString)
{
connectionString = DenormalizeConnectionString(connectionString);
@ -677,7 +684,10 @@ namespace Oqtane.Infrastructure
public void UpdateDatabaseType(string databaseType)
{
_configManager.AddOrUpdateSetting($"{SettingKeys.DatabaseSection}:{SettingKeys.DatabaseTypeKey}", databaseType, true);
if (_config.GetSetting($"{SettingKeys.DatabaseSection}:{SettingKeys.DatabaseTypeKey}", "") != databaseType)
{
_configManager.AddOrUpdateSetting($"{SettingKeys.DatabaseSection}:{SettingKeys.DatabaseTypeKey}", databaseType, true);
}
}
public void AddEFMigrationsHistory(ISqlRepository sql, string connectionString, string databaseType, string version, bool isMaster)

View File

@ -0,0 +1,28 @@
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Oqtane.Databases.Interfaces;
using Oqtane.Migrations.EntityBuilders;
using Oqtane.Repository;
namespace Oqtane.Migrations.Master
{
[DbContext(typeof(MasterDBContext))]
[Migration("Master.06.00.02.01")]
public class AddThemeVersion : MultiDatabaseMigration
{
public AddThemeVersion(IDatabase database) : base(database)
{
}
protected override void Up(MigrationBuilder migrationBuilder)
{
var themeEntityBuilder = new ThemeEntityBuilder(migrationBuilder, ActiveDatabase);
themeEntityBuilder.AddStringColumn("Version", 50, true);
}
protected override void Down(MigrationBuilder migrationBuilder)
{
// not implemented
}
}
}

View File

@ -101,6 +101,7 @@ namespace Oqtane.Repository
ModuleDefinition.Resources = moduleDefinition.Resources;
ModuleDefinition.IsEnabled = moduleDefinition.IsEnabled;
ModuleDefinition.PackageName = moduleDefinition.PackageName;
ModuleDefinition.Hash = Utilities.GenerateSimpleHash(moduleDefinition.ModifiedOn.ToString("yyyyMMddHHmm"));
}
return ModuleDefinition;

View File

@ -4,6 +4,7 @@ using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Reflection.Metadata;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Caching.Memory;
using Oqtane.Infrastructure;
@ -87,6 +88,7 @@ namespace Oqtane.Repository
Theme.ThemeSettingsType = theme.ThemeSettingsType;
Theme.ContainerSettingsType = theme.ContainerSettingsType;
Theme.PackageName = theme.PackageName;
Theme.Hash = Utilities.GenerateSimpleHash(theme.ModifiedOn.ToString("yyyyMMddHHmm"));
Themes.Add(Theme);
}
@ -126,6 +128,13 @@ namespace Oqtane.Repository
}
else
{
if (theme.Version != Theme.Version)
{
// update theme version
theme.Version = Theme.Version;
_db.SaveChanges();
}
// override user customizable property values
Theme.Name = (!string.IsNullOrEmpty(theme.Name)) ? theme.Name : Theme.Name;

View File

@ -29,12 +29,13 @@ namespace Oqtane.Services
private readonly ISettingRepository _settings;
private readonly ITenantManager _tenantManager;
private readonly ISyncManager _syncManager;
private readonly IConfigManager _configManager;
private readonly ILogManager _logger;
private readonly IMemoryCache _cache;
private readonly IHttpContextAccessor _accessor;
private readonly string _private = "[PRIVATE]";
public ServerSiteService(ISiteRepository sites, IPageRepository pages, IThemeRepository themes, IPageModuleRepository pageModules, IModuleDefinitionRepository moduleDefinitions, ILanguageRepository languages, IUserPermissions userPermissions, ISettingRepository settings, ITenantManager tenantManager, ISyncManager syncManager, ILogManager logger, IMemoryCache cache, IHttpContextAccessor accessor)
public ServerSiteService(ISiteRepository sites, IPageRepository pages, IThemeRepository themes, IPageModuleRepository pageModules, IModuleDefinitionRepository moduleDefinitions, ILanguageRepository languages, IUserPermissions userPermissions, ISettingRepository settings, ITenantManager tenantManager, ISyncManager syncManager, IConfigManager configManager, ILogManager logger, IMemoryCache cache, IHttpContextAccessor accessor)
{
_sites = sites;
_pages = pages;
@ -46,6 +47,7 @@ namespace Oqtane.Services
_settings = settings;
_tenantManager = tenantManager;
_syncManager = syncManager;
_configManager = configManager;
_logger = logger;
_cache = cache;
_accessor = accessor;
@ -143,6 +145,9 @@ namespace Oqtane.Services
// themes
site.Themes = _themes.FilterThemes(_themes.GetThemes().ToList());
// installation date used for fingerprinting static assets
site.Hash = Utilities.GenerateSimpleHash(_configManager.GetSetting("InstallationDate", DateTime.UtcNow.ToString("yyyyMMddHHmm")));
}
else
{