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) if (themetype != null)
{ {
// get resources for theme (ITheme) // 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; var themeobject = Activator.CreateInstance(themetype) as IThemeControl;
if (themeobject != null) if (themeobject != null)
@ -401,7 +401,7 @@
panes = themeobject.Panes; panes = themeobject.Panes;
} }
// get resources for theme control // 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 // theme settings components are dynamically loaded within the framework Page Management module
@ -411,7 +411,7 @@
if (settingsType != null) if (settingsType != null)
{ {
var objSettings = Activator.CreateInstance(settingsType) as IModuleControl; 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))) 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 // handle default action
if (action == Constants.DefaultAction && !string.IsNullOrEmpty(module.ModuleDefinition.DefaultAction)) if (action == Constants.DefaultAction && !string.IsNullOrEmpty(module.ModuleDefinition.DefaultAction))
@ -504,7 +504,7 @@
module.RenderMode = moduleobject.RenderMode; module.RenderMode = moduleobject.RenderMode;
module.Prerender = moduleobject.Prerender; 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 // settings components are dynamically loaded within the framework Settings module
if (action.ToLower() == "settings" && module.ModuleDefinition != null) if (action.ToLower() == "settings" && module.ModuleDefinition != null)
@ -525,7 +525,7 @@
if (moduletype != null) if (moduletype != null)
{ {
moduleobject = Activator.CreateInstance(moduletype) as IModuleControl; 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 // container settings component
@ -536,7 +536,7 @@
if (moduletype != null) if (moduletype != null)
{ {
moduleobject = Activator.CreateInstance(moduletype) as IModuleControl; 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); 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) if (resources != null)
{ {
@ -615,7 +615,7 @@
// ensure resource does not exist already // ensure resource does not exist already
if (!pageresources.Exists(item => item.Url.ToLower() == resource.Url.ToLower())) 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 charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<base href="/" /> <base href="/" />
<link rel="stylesheet" href="css/app.css" /> <link rel="stylesheet" href="css/app.css?v=@_hash" />
@if (_scripts.Contains("PWA Manifest")) @if (_scripts.Contains("PWA Manifest"))
{ {
<link id="app-manifest" rel="manifest" /> <link id="app-manifest" rel="manifest" />
@ -70,15 +70,15 @@
} }
<script src="_framework/blazor.web.js"></script> <script src="_framework/blazor.web.js"></script>
<script src="js/app.js"></script> <script src="js/app.js?v=@_hash"></script>
<script src="js/loadjs.min.js"></script> <script src="js/loadjs.min.js?v=@_hash"></script>
<script src="js/interop.js"></script> <script src="js/interop.js?v=@_hash"></script>
@((MarkupString)_scripts) @((MarkupString)_scripts)
@((MarkupString)_bodyResources) @((MarkupString)_bodyResources)
@if (_renderMode == RenderModes.Static) @if (_renderMode == RenderModes.Static)
{ {
<page-script src="./js/reload.js"></page-script> <page-script src="./js/reload.js?v=@_hash"></page-script>
} }
} }
else else
@ -94,6 +94,7 @@
private string _renderMode = RenderModes.Interactive; private string _renderMode = RenderModes.Interactive;
private string _runtime = Runtimes.Server; private string _runtime = Runtimes.Server;
private bool _prerender = true; private bool _prerender = true;
private string _hash = "";
private int _visitorId = -1; private int _visitorId = -1;
private string _antiForgeryToken = ""; private string _antiForgeryToken = "";
private string _remoteIPAddress = ""; private string _remoteIPAddress = "";
@ -136,6 +137,8 @@
_renderMode = site.RenderMode; _renderMode = site.RenderMode;
_runtime = site.Runtime; _runtime = site.Runtime;
_prerender = site.Prerender; _prerender = site.Prerender;
_hash = site.Hash;
var modules = new List<Module>(); var modules = new List<Module>();
Route route = new Route(url, alias.Path); Route route = new Route(url, alias.Path);
@ -605,13 +608,13 @@
var theme = site.Themes.FirstOrDefault(item => item.Themes.Any(item => item.TypeName == themeType)); var theme = site.Themes.FirstOrDefault(item => item.Themes.Any(item => item.TypeName == themeType));
if (theme != null) 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 else
{ {
// fallback to default Oqtane theme // fallback to default Oqtane theme
theme = site.Themes.FirstOrDefault(item => item.Themes.Any(item => item.TypeName == Constants.DefaultTheme)); 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); var type = Type.GetType(themeType);
if (type != null) if (type != null)
@ -619,7 +622,7 @@
var obj = Activator.CreateInstance(type) as IThemeControl; var obj = Activator.CreateInstance(type) as IThemeControl;
if (obj != null) 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 // theme settings components are dynamically loaded within the framework Page Management module
@ -629,7 +632,7 @@
if (settingsType != null) if (settingsType != null)
{ {
var objSettings = Activator.CreateInstance(settingsType) as IModuleControl; 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 = ""; var typename = "";
if (module.ModuleDefinition != null) 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 // handle default action
if (action == Constants.DefaultAction && !string.IsNullOrEmpty(module.ModuleDefinition.DefaultAction)) if (action == Constants.DefaultAction && !string.IsNullOrEmpty(module.ModuleDefinition.DefaultAction))
@ -684,7 +687,7 @@
var moduleobject = Activator.CreateInstance(moduletype) as IModuleControl; var moduleobject = Activator.CreateInstance(moduletype) as IModuleControl;
if (moduleobject != null) 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 // settings components are dynamically loaded within the framework Settings module
if (action.ToLower() == "settings" && module.ModuleDefinition != null) if (action.ToLower() == "settings" && module.ModuleDefinition != null)
@ -705,7 +708,7 @@
if (moduletype != null) if (moduletype != null)
{ {
moduleobject = Activator.CreateInstance(moduletype) as IModuleControl; 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 // container settings component
@ -715,7 +718,7 @@
if (moduletype != null) if (moduletype != null)
{ {
moduleobject = Activator.CreateInstance(moduletype) as IModuleControl; 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) 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; 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) if (resources != null)
{ {
@ -759,7 +762,7 @@
// ensure resource does not exist already // ensure resource does not exist already
if (!pageresources.Exists(item => item.Url.ToLower() == resource.Url.ToLower())) 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); DateTime lastwritetime = System.IO.File.GetLastWriteTime(filepath);
if (hashfilename) if (hashfilename)
{ {
HashedName = GetDeterministicHashCode(filepath).ToString("X8") + "." + lastwritetime.ToString("yyyyMMddHHmmss") + Path.GetExtension(filepath); HashedName = Utilities.GenerateSimpleHash(filepath) + "." + lastwritetime.ToString("yyyyMMddHHmmss") + Path.GetExtension(filepath);
} }
else else
{ {
@ -297,25 +297,5 @@ namespace Oqtane.Controllers
public string FilePath { get; private set; } public string FilePath { get; private set; }
public string HashedName { 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) if (user != null)
{ {
email = user.Email; 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(); installationid = Guid.NewGuid().ToString();
AddOrUpdateSetting("InstallationId", installationid, true); 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; return installationid;
} }
} }

View File

@ -265,6 +265,7 @@ namespace Oqtane.Infrastructure
var installation = IsInstalled(); var installation = IsInstalled();
try try
{ {
UpdateInstallation();
UpdateConnectionString(install.ConnectionString); UpdateConnectionString(install.ConnectionString);
UpdateDatabaseType(install.DatabaseType); UpdateDatabaseType(install.DatabaseType);
@ -491,6 +492,7 @@ namespace Oqtane.Infrastructure
moduleDefinition.Categories = moduledef.Categories; moduleDefinition.Categories = moduledef.Categories;
// update version // update version
moduleDefinition.Version = versions[versions.Length - 1]; moduleDefinition.Version = versions[versions.Length - 1];
moduleDefinition.ModifiedOn = DateTime.UtcNow;
db.Entry(moduleDefinition).State = EntityState.Modified; db.Entry(moduleDefinition).State = EntityState.Modified;
db.SaveChanges(); db.SaveChanges();
} }
@ -666,6 +668,11 @@ namespace Oqtane.Infrastructure
return connectionString; return connectionString;
} }
public void UpdateInstallation()
{
_config.GetInstallationId();
}
public void UpdateConnectionString(string connectionString) public void UpdateConnectionString(string connectionString)
{ {
connectionString = DenormalizeConnectionString(connectionString); connectionString = DenormalizeConnectionString(connectionString);
@ -677,7 +684,10 @@ namespace Oqtane.Infrastructure
public void UpdateDatabaseType(string databaseType) 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) 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.Resources = moduleDefinition.Resources;
ModuleDefinition.IsEnabled = moduleDefinition.IsEnabled; ModuleDefinition.IsEnabled = moduleDefinition.IsEnabled;
ModuleDefinition.PackageName = moduleDefinition.PackageName; ModuleDefinition.PackageName = moduleDefinition.PackageName;
ModuleDefinition.Hash = Utilities.GenerateSimpleHash(moduleDefinition.ModifiedOn.ToString("yyyyMMddHHmm"));
} }
return ModuleDefinition; return ModuleDefinition;

View File

@ -4,6 +4,7 @@ using System.Diagnostics;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Reflection; using System.Reflection;
using System.Reflection.Metadata;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Caching.Memory;
using Oqtane.Infrastructure; using Oqtane.Infrastructure;
@ -87,6 +88,7 @@ namespace Oqtane.Repository
Theme.ThemeSettingsType = theme.ThemeSettingsType; Theme.ThemeSettingsType = theme.ThemeSettingsType;
Theme.ContainerSettingsType = theme.ContainerSettingsType; Theme.ContainerSettingsType = theme.ContainerSettingsType;
Theme.PackageName = theme.PackageName; Theme.PackageName = theme.PackageName;
Theme.Hash = Utilities.GenerateSimpleHash(theme.ModifiedOn.ToString("yyyyMMddHHmm"));
Themes.Add(Theme); Themes.Add(Theme);
} }
@ -126,6 +128,13 @@ namespace Oqtane.Repository
} }
else else
{ {
if (theme.Version != Theme.Version)
{
// update theme version
theme.Version = Theme.Version;
_db.SaveChanges();
}
// override user customizable property values // override user customizable property values
Theme.Name = (!string.IsNullOrEmpty(theme.Name)) ? theme.Name : Theme.Name; 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 ISettingRepository _settings;
private readonly ITenantManager _tenantManager; private readonly ITenantManager _tenantManager;
private readonly ISyncManager _syncManager; private readonly ISyncManager _syncManager;
private readonly IConfigManager _configManager;
private readonly ILogManager _logger; private readonly ILogManager _logger;
private readonly IMemoryCache _cache; private readonly IMemoryCache _cache;
private readonly IHttpContextAccessor _accessor; private readonly IHttpContextAccessor _accessor;
private readonly string _private = "[PRIVATE]"; 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; _sites = sites;
_pages = pages; _pages = pages;
@ -46,6 +47,7 @@ namespace Oqtane.Services
_settings = settings; _settings = settings;
_tenantManager = tenantManager; _tenantManager = tenantManager;
_syncManager = syncManager; _syncManager = syncManager;
_configManager = configManager;
_logger = logger; _logger = logger;
_cache = cache; _cache = cache;
_accessor = accessor; _accessor = accessor;
@ -143,6 +145,9 @@ namespace Oqtane.Services
// themes // themes
site.Themes = _themes.FilterThemes(_themes.GetThemes().ToList()); 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 else
{ {

View File

@ -65,7 +65,7 @@ namespace Oqtane.Models
public string Categories { get; set; } public string Categories { get; set; }
/// <summary> /// <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> /// </summary>
public string Version { get; set; } public string Version { get; set; }
@ -144,6 +144,9 @@ namespace Oqtane.Models
[NotMapped] [NotMapped]
public bool IsPortable { get; set; } public bool IsPortable { get; set; }
[NotMapped]
public string Hash { get; set; }
#region Deprecated Properties #region Deprecated Properties
[Obsolete("The Permissions property is deprecated. Use PermissionList instead", false)] [Obsolete("The Permissions property is deprecated. Use PermissionList instead", false)]

View File

@ -83,7 +83,21 @@ namespace Oqtane.Models
/// </summary> /// </summary>
public string Namespace { get; set; } 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(); var resource = new Resource();
resource.ResourceType = ResourceType; resource.ResourceType = ResourceType;
@ -106,6 +120,7 @@ namespace Oqtane.Models
} }
resource.Level = level; resource.Level = level;
resource.Namespace = name; resource.Namespace = name;
resource.Version = version;
return resource; return resource;
} }

View File

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

View File

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

View File

@ -575,7 +575,6 @@ namespace Oqtane.Shared
} }
else if (expiryDate.HasValue) else if (expiryDate.HasValue)
{ {
// Include equality check here
return currentUtcTime <= expiryDate.Value; return currentUtcTime <= expiryDate.Value;
} }
else else
@ -586,32 +585,40 @@ namespace Oqtane.Shared
public static bool ValidateEffectiveExpiryDates(DateTime? effectiveDate, DateTime? expiryDate) public static bool ValidateEffectiveExpiryDates(DateTime? effectiveDate, DateTime? expiryDate)
{ {
// Treat DateTime.MinValue as null
effectiveDate ??= DateTime.MinValue; effectiveDate ??= DateTime.MinValue;
expiryDate ??= DateTime.MinValue; expiryDate ??= DateTime.MinValue;
// Check if both effectiveDate and expiryDate have values
if (effectiveDate != DateTime.MinValue && expiryDate != DateTime.MinValue) if (effectiveDate != DateTime.MinValue && expiryDate != DateTime.MinValue)
{ {
return effectiveDate <= expiryDate; return effectiveDate <= expiryDate;
} }
// Check if only effectiveDate has a value
else if (effectiveDate != DateTime.MinValue) else if (effectiveDate != DateTime.MinValue)
{ {
return true; return true;
} }
// Check if only expiryDate has a value
else if (expiryDate != DateTime.MinValue) else if (expiryDate != DateTime.MinValue)
{ {
return true; return true;
} }
// If neither effectiveDate nor expiryDate has a value, consider the page/module visible
else else
{ {
return true; 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)] [Obsolete("ContentUrl(Alias alias, int fileId) is deprecated. Use FileUrl(Alias alias, int fileId) instead.", false)]
public static string ContentUrl(Alias alias, int fileId) public static string ContentUrl(Alias alias, int fileId)
{ {