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

@ -391,7 +391,7 @@
if (themetype != null)
{
// get resources for theme (ITheme)
page.Resources = ManagePageResources(page.Resources, theme.Resources, ResourceLevel.Page, alias, "Themes", Utilities.GetTypeName(theme.ThemeName));
page.Resources = ManagePageResources(page.Resources, theme.Resources, ResourceLevel.Page, alias, "Themes", Utilities.GetTypeName(theme.ThemeName), theme.Hash);
var themeobject = Activator.CreateInstance(themetype) as IThemeControl;
if (themeobject != null)
@ -401,7 +401,7 @@
panes = themeobject.Panes;
}
// get resources for theme control
page.Resources = ManagePageResources(page.Resources, themeobject.Resources, ResourceLevel.Page, alias, "Themes", themetype.Namespace);
page.Resources = ManagePageResources(page.Resources, themeobject.Resources, ResourceLevel.Page, alias, "Themes", themetype.Namespace, theme.Hash);
}
}
// theme settings components are dynamically loaded within the framework Page Management module
@ -411,7 +411,7 @@
if (settingsType != null)
{
var objSettings = Activator.CreateInstance(settingsType) as IModuleControl;
page.Resources = ManagePageResources(page.Resources, objSettings.Resources, ResourceLevel.Module, alias, "Modules", settingsType.Namespace);
page.Resources = ManagePageResources(page.Resources, objSettings.Resources, ResourceLevel.Module, alias, "Modules", settingsType.Namespace, theme.Hash);
}
}
@ -455,7 +455,7 @@
if (module.ModuleDefinition != null && (module.ModuleDefinition.Runtimes == "" || module.ModuleDefinition.Runtimes.Contains(Runtime)))
{
page.Resources = ManagePageResources(page.Resources, module.ModuleDefinition.Resources, ResourceLevel.Module, alias, "Modules", Utilities.GetTypeName(module.ModuleDefinition.ModuleDefinitionName));
page.Resources = ManagePageResources(page.Resources, module.ModuleDefinition.Resources, ResourceLevel.Module, alias, "Modules", Utilities.GetTypeName(module.ModuleDefinition.ModuleDefinitionName), module.ModuleDefinition.Hash);
// handle default action
if (action == Constants.DefaultAction && !string.IsNullOrEmpty(module.ModuleDefinition.DefaultAction))
@ -504,7 +504,7 @@
module.RenderMode = moduleobject.RenderMode;
module.Prerender = moduleobject.Prerender;
page.Resources = ManagePageResources(page.Resources, moduleobject.Resources, ResourceLevel.Module, alias, "Modules", moduletype.Namespace);
page.Resources = ManagePageResources(page.Resources, moduleobject.Resources, ResourceLevel.Module, alias, "Modules", moduletype.Namespace, module.ModuleDefinition?.Hash);
// settings components are dynamically loaded within the framework Settings module
if (action.ToLower() == "settings" && module.ModuleDefinition != null)
@ -525,7 +525,7 @@
if (moduletype != null)
{
moduleobject = Activator.CreateInstance(moduletype) as IModuleControl;
page.Resources = ManagePageResources(page.Resources, moduleobject.Resources, ResourceLevel.Module, alias, "Modules", moduletype.Namespace);
page.Resources = ManagePageResources(page.Resources, moduleobject.Resources, ResourceLevel.Module, alias, "Modules", moduletype.Namespace, module.ModuleDefinition?.Hash);
}
// container settings component
@ -536,7 +536,7 @@
if (moduletype != null)
{
moduleobject = Activator.CreateInstance(moduletype) as IModuleControl;
page.Resources = ManagePageResources(page.Resources, moduleobject.Resources, ResourceLevel.Module, alias, "Modules", moduletype.Namespace);
page.Resources = ManagePageResources(page.Resources, moduleobject.Resources, ResourceLevel.Module, alias, "Modules", moduletype.Namespace, theme.Hash);
}
}
}
@ -595,7 +595,7 @@
return (page, modules);
}
private List<Resource> ManagePageResources(List<Resource> pageresources, List<Resource> resources, ResourceLevel level, Alias alias, string type, string name)
private List<Resource> ManagePageResources(List<Resource> pageresources, List<Resource> resources, ResourceLevel level, Alias alias, string type, string name, string version)
{
if (resources != null)
{
@ -615,7 +615,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

@ -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);
@ -676,9 +683,12 @@ namespace Oqtane.Infrastructure
}
public void UpdateDatabaseType(string databaseType)
{
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
{

View File

@ -65,7 +65,7 @@ namespace Oqtane.Models
public string Categories { get; set; }
/// <summary>
/// Version information of this Module based on the DLL / NuGet package.
/// Version information of this Module based on the information stored in its assembly
/// </summary>
public string Version { get; set; }
@ -144,6 +144,9 @@ namespace Oqtane.Models
[NotMapped]
public bool IsPortable { get; set; }
[NotMapped]
public string Hash { get; set; }
#region Deprecated Properties
[Obsolete("The Permissions property is deprecated. Use PermissionList instead", false)]

View File

@ -83,7 +83,21 @@ namespace Oqtane.Models
/// </summary>
public string Namespace { get; set; }
public Resource Clone(ResourceLevel level, string name)
/// <summary>
/// The version of the theme or module that declared the resource - only used in SiteRouter
/// </summary>
public string Version
{
set
{
if (!string.IsNullOrEmpty(value) && !string.IsNullOrEmpty(Url) && !Url.Contains("?"))
{
Url += "?v=" + value;
}
}
}
public Resource Clone(ResourceLevel level, string name, string version)
{
var resource = new Resource();
resource.ResourceType = ResourceType;
@ -106,6 +120,7 @@ namespace Oqtane.Models
}
resource.Level = level;
resource.Namespace = name;
resource.Version = version;
return resource;
}

View File

@ -187,6 +187,12 @@ namespace Oqtane.Models
[NotMapped]
public List<Theme> Themes { get; set; }
/// <summary>
/// hash code for static assets
/// </summary>
[NotMapped]
public string Hash { get; set; }
public Site Clone()
{
return new Site
@ -227,7 +233,8 @@ namespace Oqtane.Models
Settings = Settings.ToDictionary(setting => setting.Key, setting => setting.Value),
Pages = Pages.ConvertAll(page => page.Clone()),
Languages = Languages.ConvertAll(language => language.Clone()),
Themes = Themes
Themes = Themes,
Hash = Hash
};
}

View File

@ -40,10 +40,13 @@ namespace Oqtane.Models
/// </summary>
public string Name { get; set; }
// additional ITheme properties
[NotMapped]
/// <summary>
/// Version information of this Theme based on the information stored in its assembly
/// </summary>
public string Version { get; set; }
// additional ITheme properties
[NotMapped]
public string Owner { get; set; }
@ -78,17 +81,25 @@ namespace Oqtane.Models
// internal properties
[NotMapped]
public int SiteId { get; set; }
[NotMapped]
public bool IsEnabled { get; set; }
[NotMapped]
public string AssemblyName { get; set; }
[NotMapped]
public List<ThemeControl> Themes { get; set; }
[NotMapped]
public List<ThemeControl> Containers { get; set; }
[NotMapped]
public string Template { get; set; }
[NotMapped]
public string Hash { get; set; }
#region Obsolete Properties
[Obsolete("This property is obsolete. Use Themes instead.", false)]

View File

@ -575,7 +575,6 @@ namespace Oqtane.Shared
}
else if (expiryDate.HasValue)
{
// Include equality check here
return currentUtcTime <= expiryDate.Value;
}
else
@ -586,32 +585,40 @@ namespace Oqtane.Shared
public static bool ValidateEffectiveExpiryDates(DateTime? effectiveDate, DateTime? expiryDate)
{
// Treat DateTime.MinValue as null
effectiveDate ??= DateTime.MinValue;
expiryDate ??= DateTime.MinValue;
// Check if both effectiveDate and expiryDate have values
if (effectiveDate != DateTime.MinValue && expiryDate != DateTime.MinValue)
{
return effectiveDate <= expiryDate;
}
// Check if only effectiveDate has a value
else if (effectiveDate != DateTime.MinValue)
{
return true;
}
// Check if only expiryDate has a value
else if (expiryDate != DateTime.MinValue)
{
return true;
}
// If neither effectiveDate nor expiryDate has a value, consider the page/module visible
else
{
return true;
}
}
public static string GenerateSimpleHash(string text)
{
unchecked // prevent overflow exception
{
int hash = 23;
foreach (char c in text)
{
hash = hash * 31 + c;
}
return hash.ToString("X8");
}
}
[Obsolete("ContentUrl(Alias alias, int fileId) is deprecated. Use FileUrl(Alias alias, int fileId) instead.", false)]
public static string ContentUrl(Alias alias, int fileId)
{