diff --git a/src/Umbraco.Core/Extensions/AssemblyExtensions.cs b/src/Umbraco.Core/Extensions/AssemblyExtensions.cs index 45ae9ceafe..b7cf65c414 100644 --- a/src/Umbraco.Core/Extensions/AssemblyExtensions.cs +++ b/src/Umbraco.Core/Extensions/AssemblyExtensions.cs @@ -1,7 +1,9 @@ // Copyright (c) Umbraco. // See LICENSE for more details. +using System.Diagnostics.CodeAnalysis; using System.Reflection; +using Umbraco.Cms.Core.Semver; namespace Umbraco.Extensions; @@ -104,4 +106,35 @@ public static class AssemblyExtensions return null; } + + /// + /// Gets the assembly informational version for the specified . + /// + /// The assembly. + /// The assembly version. + /// + /// true if the assembly information version is retrieved; otherwise, false. + /// + public static bool TryGetInformationalVersion(this Assembly assembly, [NotNullWhen(true)] out string? version) + { + AssemblyInformationalVersionAttribute? assemblyInformationalVersionAttribute = assembly.GetCustomAttribute(); + if (assemblyInformationalVersionAttribute is not null && + SemVersion.TryParse(assemblyInformationalVersionAttribute.InformationalVersion, out SemVersion? semVersion)) + { + version = semVersion.ToSemanticStringWithoutBuild(); + return true; + } + else + { + AssemblyName assemblyName = assembly.GetName(); + if (assemblyName.Version is not null) + { + version = assemblyName.Version.ToString(3); + return true; + } + } + + version = null; + return false; + } } diff --git a/src/Umbraco.Core/IO/IOHelperExtensions.cs b/src/Umbraco.Core/IO/IOHelperExtensions.cs index 7ae90e7f8e..52e1bbe0cd 100644 --- a/src/Umbraco.Core/IO/IOHelperExtensions.cs +++ b/src/Umbraco.Core/IO/IOHelperExtensions.cs @@ -1,3 +1,4 @@ +using System.Diagnostics.CodeAnalysis; using Umbraco.Cms.Core.IO; namespace Umbraco.Extensions; @@ -11,6 +12,7 @@ public static class IOHelperExtensions /// /// /// + [return: NotNullIfNotNull("path")] public static string? ResolveRelativeOrVirtualUrl(this IIOHelper ioHelper, string? path) { if (string.IsNullOrWhiteSpace(path)) diff --git a/src/Umbraco.Core/Manifest/PackageManifest.cs b/src/Umbraco.Core/Manifest/PackageManifest.cs index 7bf07cfde9..eb732509ce 100644 --- a/src/Umbraco.Core/Manifest/PackageManifest.cs +++ b/src/Umbraco.Core/Manifest/PackageManifest.cs @@ -5,7 +5,7 @@ using Umbraco.Extensions; namespace Umbraco.Cms.Core.Manifest; /// -/// Represents the content of a package manifest. +/// Represents the content of a package manifest. /// [DataContract] public class PackageManifest @@ -13,8 +13,11 @@ public class PackageManifest private string? _packageName; /// - /// An optional package name. If not specified then the directory name is used. + /// Gets or sets the name of the package. If not specified, uses the directory name instead. /// + /// + /// The name of the package. + /// [DataMember(Name = "name")] public string? PackageName { @@ -35,81 +38,132 @@ public class PackageManifest set => _packageName = value; } + /// + /// Gets or sets the package view. + /// + /// + /// The package view. + /// [DataMember(Name = "packageView")] public string? PackageView { get; set; } /// - /// Gets the source path of the manifest. + /// Gets or sets the source path of the manifest. /// + /// + /// The source path. + /// /// - /// - /// Gets the full absolute file path of the manifest, - /// using system directory separators. - /// + /// Gets the full/absolute file path of the manifest, using system directory separators. /// [IgnoreDataMember] public string Source { get; set; } = null!; /// - /// Gets or sets the version of the package + /// Gets or sets the version of the package. /// + /// + /// The version of the package. + /// [DataMember(Name = "version")] public string Version { get; set; } = string.Empty; /// - /// Gets or sets a value indicating whether telemetry is allowed + /// Gets or sets the assembly name to get the package version from. /// + /// + /// The assembly name to get the package version from. + /// + [DataMember(Name = "versionAssemblyName")] + public string? VersionAssemblyName { get; set; } + + /// + /// Gets or sets a value indicating whether telemetry is allowed. + /// + /// + /// true if package telemetry is allowed; otherwise, false. + /// [DataMember(Name = "allowPackageTelemetry")] public bool AllowPackageTelemetry { get; set; } = true; + /// + /// Gets or sets the bundle options. + /// + /// + /// The bundle options. + /// [DataMember(Name = "bundleOptions")] public BundleOptions BundleOptions { get; set; } /// - /// Gets or sets the scripts listed in the manifest. + /// Gets or sets the scripts listed in the manifest. /// + /// + /// The scripts. + /// [DataMember(Name = "javascript")] public string[] Scripts { get; set; } = Array.Empty(); /// - /// Gets or sets the stylesheets listed in the manifest. + /// Gets or sets the stylesheets listed in the manifest. /// + /// + /// The stylesheets. + /// [DataMember(Name = "css")] public string[] Stylesheets { get; set; } = Array.Empty(); /// - /// Gets or sets the property editors listed in the manifest. + /// Gets or sets the property editors listed in the manifest. /// + /// + /// The property editors. + /// [DataMember(Name = "propertyEditors")] public IDataEditor[] PropertyEditors { get; set; } = Array.Empty(); /// - /// Gets or sets the parameter editors listed in the manifest. + /// Gets or sets the parameter editors listed in the manifest. /// + /// + /// The parameter editors. + /// [DataMember(Name = "parameterEditors")] public IDataEditor[] ParameterEditors { get; set; } = Array.Empty(); /// - /// Gets or sets the grid editors listed in the manifest. + /// Gets or sets the grid editors listed in the manifest. /// + /// + /// The grid editors. + /// [DataMember(Name = "gridEditors")] public GridEditor[] GridEditors { get; set; } = Array.Empty(); /// - /// Gets or sets the content apps listed in the manifest. + /// Gets or sets the content apps listed in the manifest. /// + /// + /// The content apps. + /// [DataMember(Name = "contentApps")] public ManifestContentAppDefinition[] ContentApps { get; set; } = Array.Empty(); /// - /// Gets or sets the dashboards listed in the manifest. + /// Gets or sets the dashboards listed in the manifest. /// + /// + /// The dashboards. + /// [DataMember(Name = "dashboards")] public ManifestDashboard[] Dashboards { get; set; } = Array.Empty(); /// - /// Gets or sets the sections listed in the manifest. + /// Gets or sets the sections listed in the manifest. /// + /// + /// The sections. + /// [DataMember(Name = "sections")] public ManifestSection[] Sections { get; set; } = Array.Empty(); } diff --git a/src/Umbraco.Core/Semver/Semver.cs b/src/Umbraco.Core/Semver/Semver.cs index 3c33f43087..a8261f3054 100644 --- a/src/Umbraco.Core/Semver/Semver.cs +++ b/src/Umbraco.Core/Semver/Semver.cs @@ -1,4 +1,5 @@ -using System.Text.RegularExpressions; +using System.Text.RegularExpressions; +using System.Diagnostics.CodeAnalysis; #if !NETSTANDARD using System.Globalization; using System.Runtime.Serialization; @@ -195,7 +196,7 @@ namespace Umbraco.Cms.Core.Semver /// /// If set to true minor and patch version are required, else they default to 0. /// False when a invalid version string is passed, otherwise true. - public static bool TryParse(string version, out SemVersion? semver, bool strict = false) + public static bool TryParse(string version, [NotNullWhen(true)] out SemVersion? semver, bool strict = false) { try { diff --git a/src/Umbraco.Infrastructure/Manifest/ManifestParser.cs b/src/Umbraco.Infrastructure/Manifest/ManifestParser.cs index 887ac05dc4..0108fa107d 100644 --- a/src/Umbraco.Infrastructure/Manifest/ManifestParser.cs +++ b/src/Umbraco.Infrastructure/Manifest/ManifestParser.cs @@ -1,3 +1,6 @@ +using System.Diagnostics.CodeAnalysis; +using System.Reflection; +using System.Runtime.Loader; using System.Text; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.FileProviders; @@ -17,7 +20,7 @@ using Umbraco.Extensions; namespace Umbraco.Cms.Core.Manifest; /// -/// Parses the Main.js file and replaces all tokens accordingly. +/// Parses the Main.js file and replaces all tokens accordingly. /// public class ManifestParser : IManifestParser { @@ -39,7 +42,7 @@ public class ManifestParser : IManifestParser private string _path = null!; /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// public ManifestParser( AppCaches appCaches, @@ -163,31 +166,35 @@ public class ManifestParser : IManifestParser /// public PackageManifest ParseManifest(string text) { - if (text == null) - { - throw new ArgumentNullException(nameof(text)); - } + ArgumentNullException.ThrowIfNull(text); if (string.IsNullOrWhiteSpace(text)) { throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(text)); } - PackageManifest? manifest = JsonConvert.DeserializeObject( + PackageManifest manifest = JsonConvert.DeserializeObject( text, new DataEditorConverter(_dataValueEditorFactory, _ioHelper, _localizedTextService, _shortStringHelper, _jsonSerializer), new ValueValidatorConverter(_validators), - new DashboardAccessRuleConverter()); + new DashboardAccessRuleConverter())!; + + if (string.IsNullOrEmpty(manifest.Version) && + !string.IsNullOrEmpty(manifest.VersionAssemblyName) && + TryGetAssemblyInformationalVersion(manifest.VersionAssemblyName, out string? version)) + { + manifest.Version = version; + } // scripts and stylesheets are raw string, must process here - for (var i = 0; i < manifest!.Scripts.Length; i++) + for (var i = 0; i < manifest.Scripts.Length; i++) { - manifest.Scripts[i] = _ioHelper.ResolveRelativeOrVirtualUrl(manifest.Scripts[i])!; + manifest.Scripts[i] = _ioHelper.ResolveRelativeOrVirtualUrl(manifest.Scripts[i]); } for (var i = 0; i < manifest.Stylesheets.Length; i++) { - manifest.Stylesheets[i] = _ioHelper.ResolveRelativeOrVirtualUrl(manifest.Stylesheets[i])!; + manifest.Stylesheets[i] = _ioHelper.ResolveRelativeOrVirtualUrl(manifest.Stylesheets[i]); } foreach (ManifestContentAppDefinition contentApp in manifest.ContentApps) @@ -197,7 +204,7 @@ public class ManifestParser : IManifestParser foreach (ManifestDashboard dashboard in manifest.Dashboards) { - dashboard.View = _ioHelper.ResolveRelativeOrVirtualUrl(dashboard.View)!; + dashboard.View = _ioHelper.ResolveRelativeOrVirtualUrl(dashboard.View); } foreach (GridEditor gridEditor in manifest.GridEditors) @@ -217,6 +224,22 @@ public class ManifestParser : IManifestParser return manifest; } + private bool TryGetAssemblyInformationalVersion(string name, [NotNullWhen(true)] out string? version) + { + foreach (Assembly assembly in AssemblyLoadContext.Default.Assemblies) + { + AssemblyName assemblyName = assembly.GetName(); + if (string.Equals(assemblyName.Name, name, StringComparison.OrdinalIgnoreCase) && + assembly.TryGetInformationalVersion(out version)) + { + return true; + } + } + + version = null; + return false; + } + /// /// Merges all manifests into one. /// diff --git a/src/Umbraco.Infrastructure/Services/Implement/PackagingService.cs b/src/Umbraco.Infrastructure/Services/Implement/PackagingService.cs index a0330d75fd..5047bae233 100644 --- a/src/Umbraco.Infrastructure/Services/Implement/PackagingService.cs +++ b/src/Umbraco.Infrastructure/Services/Implement/PackagingService.cs @@ -116,8 +116,7 @@ public class PackagingService : IPackagingService public IEnumerable GetAllInstalledPackages() { - IReadOnlyDictionary? keyValues = - _keyValueService.FindByKeyPrefix(Constants.Conventions.Migrations.KeyValuePrefix); + IReadOnlyDictionary? keyValues = _keyValueService.FindByKeyPrefix(Constants.Conventions.Migrations.KeyValuePrefix); var installedPackages = new Dictionary(); @@ -126,14 +125,21 @@ public class PackagingService : IPackagingService { if (!installedPackages.TryGetValue(plan.PackageName, out InstalledPackage? installedPackage)) { - installedPackage = new InstalledPackage { PackageName = plan.PackageName }; + installedPackage = new InstalledPackage + { + PackageName = plan.PackageName, + }; + + if (plan.GetType().Assembly.TryGetInformationalVersion(out string? version)) + { + installedPackage.Version = version; + } + installedPackages.Add(plan.PackageName, installedPackage); } var currentPlans = installedPackage.PackageMigrationPlans.ToList(); - if (keyValues is null || keyValues.TryGetValue( - Constants.Conventions.Migrations.KeyValuePrefix + plan.Name, - out var currentState) is false) + if (keyValues is null || keyValues.TryGetValue(Constants.Conventions.Migrations.KeyValuePrefix + plan.Name, out var currentState) is false) { currentState = null; } @@ -157,14 +163,20 @@ public class PackagingService : IPackagingService if (!installedPackages.TryGetValue(package.PackageName, out InstalledPackage? installedPackage)) { - installedPackage = new InstalledPackage { + installedPackage = new InstalledPackage + { PackageName = package.PackageName, - Version = string.IsNullOrEmpty(package.Version) ? "Unknown" : package.Version, }; installedPackages.Add(package.PackageName, installedPackage); } + // Set additional values + if (!string.IsNullOrEmpty(package.Version)) + { + installedPackage.Version = package.Version; + } + installedPackage.PackageView = package.PackageView; } diff --git a/src/Umbraco.Web.UI.Client/src/views/packages/views/installed.html b/src/Umbraco.Web.UI.Client/src/views/packages/views/installed.html index 248e926c98..a166f7ab2d 100644 --- a/src/Umbraco.Web.UI.Client/src/views/packages/views/installed.html +++ b/src/Umbraco.Web.UI.Client/src/views/packages/views/installed.html @@ -17,7 +17,7 @@
{{ installedPackage.name }}
-
Version: {{ installedPackage.version }}
+
Version: {{ installedPackage.version }}
No pending migrations diff --git a/templates/UmbracoPackage/App_Plugins/UmbracoPackage/package.manifest b/templates/UmbracoPackage/App_Plugins/UmbracoPackage/package.manifest index 906db79b7a..d4f357f261 100644 --- a/templates/UmbracoPackage/App_Plugins/UmbracoPackage/package.manifest +++ b/templates/UmbracoPackage/App_Plugins/UmbracoPackage/package.manifest @@ -1,5 +1,5 @@ -{ +{ "name": "UmbracoPackage", - "version": "", + "versionAssemblyName": "UmbracoPackage", "allowPackageTelemetry": true -} \ No newline at end of file +} diff --git a/templates/UmbracoPackageRcl/wwwroot/package.manifest b/templates/UmbracoPackageRcl/wwwroot/package.manifest index 6aadd0cee6..d4f357f261 100644 --- a/templates/UmbracoPackageRcl/wwwroot/package.manifest +++ b/templates/UmbracoPackageRcl/wwwroot/package.manifest @@ -1,5 +1,5 @@ { "name": "UmbracoPackage", - "version": "", + "versionAssemblyName": "UmbracoPackage", "allowPackageTelemetry": true }