using System.Diagnostics.CodeAnalysis; using System.Reflection; using System.Runtime.Loader; using System.Xml.Linq; using Microsoft.Extensions.FileProviders; using Microsoft.Extensions.Hosting; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Extensions; using Umbraco.Cms.Core.Manifest; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.Membership; using Umbraco.Cms.Core.Models.Packaging; using Umbraco.Cms.Core.Notifications; using Umbraco.Cms.Core.Packaging; using Umbraco.Cms.Core.Scoping; using Umbraco.Cms.Core.Services.OperationStatus; using Umbraco.Cms.Infrastructure.Manifest; using Umbraco.Extensions; using File = System.IO.File; namespace Umbraco.Cms.Core.Services.Implement; /// /// Represents the Packaging Service, which provides import/export functionality for the Core models of the API /// using xml representation. This is primarily used by the Package functionality. /// public class PackagingService : IPackagingService { private readonly IAuditService _auditService; private readonly ICreatedPackagesRepository _createdPackages; private readonly IEventAggregator _eventAggregator; private readonly IKeyValueService _keyValueService; private readonly IPackageInstallation _packageInstallation; private readonly PackageMigrationPlanCollection _packageMigrationPlans; private readonly IPackageManifestReader _packageManifestReader; private readonly ICoreScopeProvider _coreScopeProvider; private readonly IHostEnvironment _hostEnvironment; private readonly IUserService _userService; public PackagingService( IAuditService auditService, ICreatedPackagesRepository createdPackages, IPackageInstallation packageInstallation, IEventAggregator eventAggregator, IKeyValueService keyValueService, ICoreScopeProvider coreScopeProvider, PackageMigrationPlanCollection packageMigrationPlans, IPackageManifestReader packageManifestReader, IHostEnvironment hostEnvironment, IUserService userService) { _auditService = auditService; _createdPackages = createdPackages; _packageInstallation = packageInstallation; _eventAggregator = eventAggregator; _keyValueService = keyValueService; _packageMigrationPlans = packageMigrationPlans; _packageManifestReader = packageManifestReader; _coreScopeProvider = coreScopeProvider; _hostEnvironment = hostEnvironment; _userService = userService; } #region Installation public CompiledPackage GetCompiledPackageInfo(XDocument? xml) => _packageInstallation.ReadPackage(xml); public InstallationSummary InstallCompiledPackageData(XDocument? packageXml, int userId = Constants.Security.SuperUserId) { CompiledPackage compiledPackage = GetCompiledPackageInfo(packageXml); if (compiledPackage == null) { throw new InvalidOperationException("Could not read the package file " + packageXml); } // Trigger the Importing Package Notification and stop execution if event/user is cancelling it var importingPackageNotification = new ImportingPackageNotification(compiledPackage.Name); if (_eventAggregator.PublishCancelable(importingPackageNotification)) { return new InstallationSummary(compiledPackage.Name); } InstallationSummary summary = _packageInstallation.InstallPackageData(compiledPackage, userId, out _); IUser? user = _userService.GetUserById(userId); _auditService.AddAsync( AuditType.PackagerInstall, user?.Key ?? Constants.Security.SuperUserKey, -1, "Package", $"Package data installed for package '{compiledPackage.Name}'.").GetAwaiter().GetResult(); // trigger the ImportedPackage event _eventAggregator.Publish(new ImportedPackageNotification(summary).WithStateFrom(importingPackageNotification)); return summary; } public InstallationSummary InstallCompiledPackageData(FileInfo packageXmlFile, int userId = Constants.Security.SuperUserId) { XDocument xml; using (StreamReader streamReader = File.OpenText(packageXmlFile.FullName)) { xml = XDocument.Load(streamReader); } return InstallCompiledPackageData(xml, userId); } #endregion #region Created/Installed Package Repositories /// public async Task> DeleteCreatedPackageAsync(Guid key, Guid userKey) { using ICoreScope scope = _coreScopeProvider.CreateCoreScope(); PackageDefinition? package = await GetCreatedPackageByKeyAsync(key); if (package == null) { return Attempt.FailWithStatus(PackageOperationStatus.NotFound, null); } Attempt result = await _auditService.AddAsync(AuditType.Delete, userKey, -1, "Package", $"Created package '{package.Name}' deleted. Package key: {key}"); if (result is { Success: false, Result: AuditLogOperationStatus.UserNotFound }) { throw new InvalidOperationException($"Could not find user with key: {userKey}"); } _createdPackages.Delete(package.Id); scope.Complete(); return Attempt.SucceedWithStatus(PackageOperationStatus.Success, package); } public PackageDefinition? GetCreatedPackageById(int id) { using ICoreScope scope = _coreScopeProvider.CreateCoreScope(autoComplete: true); return _createdPackages.GetById(id); } /// public Task> GetCreatedPackagesAsync(int skip, int take) { using ICoreScope scope = _coreScopeProvider.CreateCoreScope(autoComplete: true); PackageDefinition[] packages = _createdPackages.GetAll().WhereNotNull().ToArray(); var pagedModel = new PagedModel(packages.Length, packages.Skip(skip).Take(take)); return Task.FromResult(pagedModel); } /// public Task GetCreatedPackageByKeyAsync(Guid key) { using ICoreScope scope = _coreScopeProvider.CreateCoreScope(autoComplete: true); return Task.FromResult(_createdPackages.GetByKey(key)); } /// public async Task> CreateCreatedPackageAsync(PackageDefinition package, Guid userKey) { using ICoreScope scope = _coreScopeProvider.CreateCoreScope(); if (_createdPackages.SavePackage(package) == false) { if (string.IsNullOrEmpty(package.Name)) { return Attempt.FailWithStatus(PackageOperationStatus.InvalidName, package); } return Attempt.FailWithStatus(PackageOperationStatus.DuplicateItemName, package); } Attempt result = await _auditService.AddAsync(AuditType.New, userKey, -1, "Package", $"Created package '{package.Name}' created. Package key: {package.PackageId}"); if (result is { Success: false, Result: AuditLogOperationStatus.UserNotFound }) { throw new InvalidOperationException($"Could not find user with key: {userKey}"); } scope.Complete(); return Attempt.SucceedWithStatus(PackageOperationStatus.Success, package); } /// public async Task> UpdateCreatedPackageAsync(PackageDefinition package, Guid userKey) { using ICoreScope scope = _coreScopeProvider.CreateCoreScope(); if (_createdPackages.SavePackage(package) == false) { return Attempt.FailWithStatus(PackageOperationStatus.NotFound, package); } Attempt result = await _auditService.AddAsync(AuditType.New, userKey, -1, "Package", $"Created package '{package.Name}' updated. Package key: {package.PackageId}"); if (result is { Success: false, Result: AuditLogOperationStatus.UserNotFound }) { throw new InvalidOperationException($"Could not find user with key: {userKey}"); } scope.Complete(); return Attempt.SucceedWithStatus(PackageOperationStatus.Success, package); } public string ExportCreatedPackage(PackageDefinition definition) { using ICoreScope scope = _coreScopeProvider.CreateCoreScope(autoComplete: true); return _createdPackages.ExportPackage(definition); } public InstalledPackage? GetInstalledPackageByName(string packageName) => GetAllInstalledPackagesAsync().GetAwaiter().GetResult().Where(x => x.PackageName?.InvariantEquals(packageName) ?? false).FirstOrDefault(); public async Task> GetAllInstalledPackagesAsync() { using ICoreScope scope = _coreScopeProvider.CreateCoreScope(autoComplete: true); IReadOnlyDictionary? keyValues = _keyValueService.FindByKeyPrefix(Constants.Conventions.Migrations.KeyValuePrefix); var installedPackages = new List(); // Collect the package from the package migration plans foreach (PackageMigrationPlan plan in _packageMigrationPlans) { InstalledPackage installedPackage; if (plan.PackageId is not null && installedPackages.FirstOrDefault(x => x.PackageId == plan.PackageId) is InstalledPackage installedPackageById) { installedPackage = installedPackageById; } else if (installedPackages.FirstOrDefault(x => x.PackageName == plan.PackageName) is InstalledPackage installedPackageByName) { installedPackage = installedPackageByName; // Ensure package ID is set installedPackage.PackageId ??= plan.PackageId; } else { installedPackage = new InstalledPackage { PackageId = plan.PackageId, PackageName = plan.PackageName, }; installedPackages.Add(installedPackage); } if (installedPackage.Version is null && plan.GetType().Assembly.TryGetInformationalVersion(out string? version)) { installedPackage.Version = version; } // Combine all package migration plans for a package var currentPlans = installedPackage.PackageMigrationPlans.ToList(); if (keyValues is null || keyValues.TryGetValue(Constants.Conventions.Migrations.KeyValuePrefix + plan.Name, out var currentState) is false) { currentState = null; } currentPlans.Add(new InstalledPackageMigrationPlans { CurrentMigrationId = currentState, FinalMigrationId = plan.FinalState, }); installedPackage.PackageMigrationPlans = currentPlans; } // Collect and merge the packages from the manifests foreach (PackageManifest packageManifest in await _packageManifestReader.ReadPackageManifestsAsync().ConfigureAwait(false)) { if (packageManifest.Id is null && string.IsNullOrEmpty(packageManifest.Name)) { continue; } InstalledPackage installedPackage; if (packageManifest.Id is not null && installedPackages.FirstOrDefault(x => x.PackageId == packageManifest.Id) is InstalledPackage installedPackageById) { installedPackage = installedPackageById; // Always use package name from manifest installedPackage.PackageName = packageManifest.Name; } else if (installedPackages.FirstOrDefault(x => x.PackageName == packageManifest.Name) is InstalledPackage installedPackageByName) { installedPackage = installedPackageByName; // Ensure package ID is set installedPackage.PackageId ??= packageManifest.Id; } else { installedPackage = new InstalledPackage { PackageId = packageManifest.Id, PackageName = packageManifest.Name, }; installedPackages.Add(installedPackage); } // Set additional values installedPackage.AllowPackageTelemetry = packageManifest is { AllowTelemetry: true, AllowPackageTelemetry: true }; if (!string.IsNullOrEmpty(packageManifest.Version)) { // Always use package version from manifest installedPackage.Version = packageManifest.Version; } else if (string.IsNullOrEmpty(installedPackage.Version) && string.IsNullOrEmpty(installedPackage.PackageId) is false && TryGetAssemblyInformationalVersion(installedPackage.PackageId, out string? version)) { // Use version of the assembly with the same name as the package ID installedPackage.Version = version; } } // Return all packages with an ID or name in the package manifest or package migrations return installedPackages; } #endregion /// public Task> GetInstalledPackagesFromMigrationPlansAsync(int skip, int take) { IReadOnlyDictionary keyValues = _keyValueService.FindByKeyPrefix(Constants.Conventions.Migrations.KeyValuePrefix) ?? new Dictionary(); InstalledPackage[] installedPackages = _packageMigrationPlans .GroupBy(plan => (plan.PackageName, plan.PackageId)) .Select(group => { var package = new InstalledPackage { PackageName = group.Key.PackageName, }; package.PackageMigrationPlans = group .Select(plan => { // look for migration states in this order: // - plan name // - package identifier // - package name var currentState = keyValues.GetValueOrDefault($"{Constants.Conventions.Migrations.KeyValuePrefix}{plan.Name}") ?? keyValues.GetValueOrDefault($"{Constants.Conventions.Migrations.KeyValuePrefix}{plan.PackageId ?? plan.PackageName}"); return new InstalledPackageMigrationPlans { CurrentMigrationId = currentState, FinalMigrationId = plan.FinalState, }; }); return package; }).ToArray(); return Task.FromResult(new PagedModel { Total = installedPackages.Count(), Items = installedPackages.Skip(skip).Take(take), }); } /// public Stream? GetPackageFileStream(PackageDefinition definition) { // Removing the ContentRootPath from the package path as a sub path is required for GetFileInfo() var subPath = definition.PackagePath.Replace(_hostEnvironment.ContentRootPath, string.Empty); IFileInfo packageFile = _hostEnvironment.ContentRootFileProvider.GetFileInfo(subPath); if (packageFile.Exists == false) { return null; } return packageFile.CreateReadStream(); } private static 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; } }